起因
使用SmartPhone上的WinForm做了一个WM的小程序,结果放到手机上实际一运行。发现动态生成的控件在里面显示得都非常小,难以看清。
原因
我的问题是需要在InitializeComponent方法结束后,动态生成一些控件,如下:
/// <summary> /// 这个方法会根据传入的实体模型,生成一些选择框,设置它们的大小、位置;并会改变其它控件的大小、位置。 /// </summary> /// <param name="categories"></param> private void GenerateCheckBoxes(IList<Category> categories) {
…… }
原因就是因为手机分辨率较大,而这些动态生成的控件并没有进行随着分辨率不同而进行自动缩放。而由界面设计器设计出来的控件,都能很好的显示。
求索
由于界面生成的控件能够很好的自适应分辨率的不同,所以先看一下Designer生成的代码:
private void InitializeComponent() { this.BAdd = new System.Windows.Forms.Button(); this.PCategories = new System.Windows.Forms.Panel(); this.SuspendLayout();
// BAdd this.BAdd.Location = new System.Drawing.Point(165, 164); this.BAdd.Name = "BAdd"; this.BAdd.Size = new System.Drawing.Size(72, 20); this.BAdd.TabIndex = 11; this.BAdd.Text = "Add"; this.BAdd.Click += new System.EventHandler(this.BAdd_Click); // PCategories this.PCategories.Location = new System.Drawing.Point(73, 83); this.PCategories.Name = "PCategories"; this.PCategories.Size = new System.Drawing.Size(164, 75); // MainForm this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; this.AutoScroll = true; this.ClientSize = new System.Drawing.Size(243, 258); this.Controls.Add(this.PCategories); this.Controls.Add(this.BAdd); this.Name = "MainForm"; this.Text = "MoneyManagerForm"; this.ResumeLayout(false); }
这里的重点是使用了AutoScaleDimensions和AutoScaleMode属性来设置界面为自动缩放。(Dpi表示Dot per inch,WPF就是直接使用这种方式来控制界面的。)然后最后一步调用ResumeLayout方法,这个方法中,会调用到ContainerControl.PerformAutoScale方法进行自动缩放。
最可恶的一点:从控件的构造,到界面的自动缩放,全部在一个方法中实现!而且这个方法中,没有什么好的办法来调用我生成控件的方法……
解决过程
在Form中,重写ScaleControl方法如下:
protected override void ScaleControl(SizeF factor, BoundsSpecified specified) { var categories = Config.Instance.Categories; this.GenerateCheckBoxes(categories); base.ScaleControl(factor, specified); }
因为调用过程是这样的:
Control.LayoutResume() –> ContainerControl.PerformAutoScale() –> Control.Scale() –> Control.ScaleControl() & Control.ScaleChildControls()。
所以,只需要重写这个方法,就可以在真正执行自动缩放所有控件前,先把动态控件生成。
不过,这样做同样有局限性:因为这里是在InitializeComponent方法中进行PerformAutoScale,所以这里的这些动态生成的控件,其实是在应用程序的开始阶段就已经被明确了。相反,如果在运行一段时间后,需要想再动态生成其它控件,就不能使用这个方法了。那时,就需要直接调用刚生成的需要缩放的控件的Scale方法。而且不能直接使用PerformAutoScale方法了,因为要保证一个控件只被调用Scale方法一次! 在这里,只需要这样简单实现就行了。:)
另外,一开始以为PerformAutoScale并不会把缩放过的控件,再缩放一次,结果就写成了这样的错误方案:
public MainForm()
{
InitializeComponent();
//暂停布局
this.SuspendLayout();
//在InitComponents调用的PerformAutoScale方法里面,最后会把这个数给置为运行时的数据。所以这里需要重新设置。 this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); //生成控件 this.GenerateCategories(); //自动缩放 this.PerformAutoScale();//其实这里会把InitializeComponent中缩放的控件都再缩放一遍。 //继续布局 this.ResumeLayout(false); }
结束语
其实,这里的自动缩放过程,在WinForm开发中,也是一样的。
而且这次实践中,我还发现:我在Win7的系统上随手点了一下这个程序,居然所有功能都能够正常的运行……汗,当时做的时候,可是专门为WindowsMobile开发的窗体啊。这个“跨平台”功能,确实很强大,着实让我吃惊不小。
另外,由于VS2008自带的模拟器的屏幕分辨率和设计时的分辨率是一样大的,而我手机的分辨率比这个要大得多。所以每次调试这个缩放过程时,都要生成好了,然后拷贝到手机上看效果,真是吐血……
引用
Windows 窗体现在使用下面的逻辑自动对窗体及其内容进行缩放:
设计时,每一个 ContainerControl 分别在 AutoScaleMode 和 AutoScaleDimensions 中记录缩放模式和它的当前分辨率。
运行时,实际分辨率存储在 CurrentAutoScaleDimensions 属性中。AutoScaleFactor 属性会动态计算运行时分辨率与设计时分辨率的比值。
当加载窗体时,如果 CurrentAutoScaleDimensions 和 AutoScaleDimensions 的值不同,则会调用 PerformAutoScale 方法对该控件及其子控件进行缩放。此方法会挂起布局并调用 Scale 方法执行实际缩放。然后,会更新 AutoScaleDimensions 值以避免累进缩放。
在下面的情况下还会自动调用 PerformAutoScale:
在缩放模式为 Font 时响应 OnFontChanged 事件。
当继续执行容器控件的布局时检测到 AutoScaleDimensions 或 AutoScaleMode 属性发生更改。
与上面的情况类似,检测到父 ContainerControl 正在被缩放。每个容器控件只负责使用自己的比例因子缩放自己的子控件,并不负责缩放其父容器中的控件。
子控件可以通过下面的若干方式修改其缩放行为:
可以重写 ScaleChildren 属性以确定是否应缩放其子控件。
可以重写 GetScaledBounds 方法以调整要将控件缩放至的边界,但不调整缩放逻辑。
可以重写 ScaleControl 方法以更改当前控件的缩放逻辑。