#C# ——窗体和控件随着分辨率的变化自适应大小
一.说明
在WinForm中,我们会经常用到控件,控件的整体布局会随着分辨率、缩放与布置的大小而改变,要解决这个问题,就是首先记录窗体和它上面控件的初始位置和大小,当窗体改变比例时,其控件的位置和大小也按此比例变化即可。因为窗体上控件的位置和大小是相对于自己所在的窗体的,也就是所谓的窗口坐标。
在这里我们只考虑相对于自己窗体的窗口坐标更简单,也就是成比例变化。为了多个窗体共用,我在这里创建一个类AutoSizeFormClass,1.使用它去记录窗体和其控件的初始位置和大小,2.根据窗体变化了的大小,成比例地实现其控件的水平和垂直方向的变化,也就是自适应。
二.使用方法(一)
使用方法很简单,
1.把自适应的类整体复制到你的工程命名空间里,
然后在需要自适应的窗体中做2、3两步即可;
2.声明自适应类实例;
3.为窗体添加大小改变事件,并在其方法中,调用类的自适应方法,完成自适应。
通过上面的介绍我们会发现其中的几个实现难点:
1.如何保存窗体的以及其中控件的位置以及大小等属性,当然我们最常用的方法就是自己定义一个实体,实体中包含我们需要保持的属性(主要包括left,top,width,height,以及fontsize属性)这个我提供的解决方案是定义一个数据结构。
2.普及一下结构体的知识:结构体中包含其中要存储的数据,使用结构体的好处在于可以将不同类型的数据有序的组合在一起,结构造出一个新的数据类型,不占内存空间,只用定义结构体的变量时才开辟内存空间,结构体类型的变量在内存依照其成员顺序顺序排列,所占内存空间的大小是其全体成员所占空间的总和,结构体可以作为函数的参数,函数也可以返回结构体。
声明一个泛型用来保存所有控件位置以及大小信息产生的结构体信息。
//这里只是列出两个容器控件分别为panel控件和groupbox控件
if (c.GetType().ToString() == "System.Windows.Forms.Panel")
{
//要执行的代码
}
//如果是GroupBox控件
if (c.GetType().ToString() == "System.Windows.Forms.GroupBox")
{
//要执行的代码
}
但我发现自己很难把所有的控件都想全了,即使是想全了也会重复太多的代码,最后采取了一个很有效的方法就是加上这个判断:
if (c.Controls.Count > 0)
一旦这个判断成立就说明这个控件就是一个容器控件了。
3.递归调用保存控件信息类,实现所有控件(包括容器控件)的位置和大小信息的保存。
4.如何保存画窗体时窗体的大小,听起来有些别嘴,其实也可以说是你想要窗体呈现的大小,这个大大家好像会有疑问但虽然说窗体是控件的一种,但是进过我的实现,当我们改变分辨率的同时,比如我们原来设置窗体每次打开时最大化显示,但是无论我们编写程序时设置的窗体的大小多大,我们运行起来时窗体都会占满整个屏幕,这也是出现由于分辨率改变,大窗体中部分控件无法显示完全的原因。
三.代码实现(一)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
namespace AutoSizeFormClass
{
public class AutoSizeFormClass
{
/// <summary>
/// 声明一个结构,用于保存控件位置的基本属性。
/// </summary>
public struct controlRect
{
/// <summary>
/// 控件的left属性
/// </summary>
public int Left;
/// <summary>
/// 控件的Right属性
/// </summary>
public int Top;
/// <summary>
/// 控件的Weight属性
/// </summary>
public int Width;
/// <summary>
/// 控件的High属性
/// </summary>
public int Height;
/// <summary>
/// 控件的Fontsize属性
/// </summary>
public float FontSize;
}
/// <summary>
/// 声明一个泛型,类型为什么的保存控件属性的结构类,
/// </summary>
public List<controlRect> oldCtrl = new List<controlRect>();
int ctrlNo = 0;//初始化标识控件的变量为0,表示窗体本身。
/// <summary>
/// 保存控件的位置和大小信息
/// </summary>
/// <param name="ctl">需要被保存的控件</param>
private void AddControl(Control ctl)
{
foreach (Control c in ctl.Controls)
{
controlRect objCtrl;
objCtrl.Left = c.Left;
objCtrl.Top = c.Top;
objCtrl.Width = c.Width;
objCtrl.Height = c.Height;
objCtrl.FontSize = c.Font.Size;
oldCtrl.Add(objCtrl);
//**放在这里,是先记录控件本身,后记录控件的子控件,重点是前后要一致
if (c.Controls.Count > 0)
AddControl(c);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
}
}
/// <summary>
/// 窗体自适应分辨率类
/// </summary>
/// <param name="mForm">需要进行设置的窗体</param>
public void controlAutoSize(Control mForm)
{
if (ctrlNo == 0)
{ //*如果在窗体的Form1_Load中,记录控件原始的大小和位置,正常没有问题,但要加入皮肤就会出现问题,因为有些控件如dataGridView的的子控件还没有完成,个数少
//*要在窗体的Form1_SizeChanged中,第一次改变大小时,记录控件原始的大小和位置,这里所有控件的子控件都已经形成
controlRect cR;
cR.Left = mForm.Left;
cR.Top = mForm.Top;
cR.Width = mForm.Width;
cR.Height = mForm.Height;
cR.Width = int.Parse(mForm.Tag.ToString().Split(',')[0]);
cR.Height = int.Parse(mForm.Tag.ToString().Split(',')[1]);
cR.FontSize = mForm.Font.Size;
oldCtrl.Add(cR);//第一个为"窗体本身",只加入一次即可
AddControl(mForm);//窗体内其余控件可能嵌套其它控件(比如panel),故单独抽出以便递归调用
}
float wScale = (float)mForm.Width / (float)oldCtrl[0].Width;//新旧窗体之间的比例,与最早的旧窗体比较
float hScale = (float)mForm.Height / (float)oldCtrl[0].Height;//.Height;
ctrlNo = 1;//进入=1,第0个为窗体本身,窗体内的控件,从序号1开始
AutoScaleControl(mForm, wScale, hScale);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
}
/// 设置控件的属性
/// </summary>
/// <param name="ctl">需要设置的控件</param>
/// <param name="wScale">调整的高度比例</param>
/// <param name="hScale">调整的宽度比例</param>
private void AutoScaleControl(Control ctl, float wScale, float hScale)
{
int ctrLeft0, ctrTop0, ctrWidth0, ctrHeight0;
float ctrFontSize0;
//第1个是窗体自身的 Left,Top,Width,Height,所以窗体控件从ctrlNo=1开始
foreach (Control c in ctl.Controls)
{
//获得控件原有的位置和大小信息
ctrLeft0 = oldCtrl[ctrlNo].Left;
ctrTop0 = oldCtrl[ctrlNo].Top;
ctrWidth0 = oldCtrl[ctrlNo].Width;
ctrHeight0 = oldCtrl[ctrlNo].Height;
ctrFontSize0 = oldCtrl[ctrlNo].FontSize;
//设置控件新的位置和大小信息。
c.Left = (int)((ctrLeft0) * wScale);//新旧控件之间的线性比例。控件位置只相对于窗体
c.Top = (int)((ctrTop0) * hScale);//
c.Width = (int)(ctrWidth0 * wScale);//只与最初的大小相关,所以不能与现在的宽度相乘
c.Height = (int)(ctrHeight0 * hScale);//
c.Font = new Font(c.Font.Name, (float)(ctrFontSize0 * wScale));//设置控件中字体的大小以适应控件的大小
ctrlNo++;//累加序号
//**放在这里,是先缩放控件本身,后缩放控件的子控件,重点是前后要一致(与保存时)
if (c.Controls.Count > 0)
AutoScaleControl(c, wScale, hScale);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
}
}
}
}
代码中的注释比较详细了,如果你想实现窗体的自适应分辨率,你只需要在窗体的Layout事件中添加如下代码:
/// <summary>
/// 声明一个窗体自适应分辨率类
/// </summary>
public AutoSizeFormClass As = new AutoSizeFormClass();
/// <summary>
/// 在窗体的layout事件中调用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_Layout(object sender, LayoutEventArgs e)
{
As.controlAutoSize(this);
}
这里用的是layout事件,不用Resize或者是SizeChanged事件,这几个事件的触发顺序是不同的首先触发的是Resize→然后是SizeChanged→然后是layout→最后是Load事件,是不是把适应分辨率的代码写在那个事件下都可以呢,这个我也尝试了,当窗体中含有tabcontrol控件时只有layout事件触发时才能检测出窗体中包含控件,这几个事件的区别我实在不知道有声明区别,希望读者给出帮助。
四.使用方法(二)
使用方法很简单,
1.把自适应的类整体复制到你的工程命名空间里,
然后在需要自适应的窗体中做3步即可:
2.声明自适应类实例。
3.为窗体添加Load事件,并在其方法Form1_Load中,调用类的初始化方法,记录窗体和其控件初始位置和大小
4.为窗体添加SizeChanged事件,并在其方法Form1_SizeChanged中,调用类的自适应方法,完成自适应
五.代码实现(二)
1.定义AutoResizeForm类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace CSharpFormApplication
{
class AutoResizeForm
{
//(1).声明结构,只记录窗体和其控件的初始位置和大小。
public struct controlRect
{
public int Left;
public int Top;
public int Width;
public int Height;
}
//(2).声明 1个对象
//注意这里不能使用控件列表记录 List nCtrl;,因为控件的关联性,记录的始终是当前的大小。
// public List oldCtrl= new List();//这里将西文的大于小于号都过滤掉了,只能改为中文的,使用中要改回西文
public List<controlRect> oldCtrl = new List<controlRect>();
int ctrlNo = 0;//1;
//(3). 创建两个函数
//(3.1)记录窗体和其控件的初始位置和大小,
public void controllInitializeSize(Control mForm)
{
controlRect cR;
cR.Left = mForm.Left; cR.Top = mForm.Top; cR.Width = mForm.Width; cR.Height = mForm.Height;
oldCtrl.Add(cR);//第一个为"窗体本身",只加入一次即可
AddControl(mForm);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
//this.WindowState = (System.Windows.Forms.FormWindowState)(2);//记录完控件的初始位置和大小后,再最大化
//0 - Normalize , 1 - Minimize,2- Maximize
}
private void AddControl(Control ctl)
{
foreach (Control c in ctl.Controls)
{ //**放在这里,是先记录控件的子控件,后记录控件本身
//if (c.Controls.Count > 0)
// AddControl(c);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
controlRect objCtrl;
objCtrl.Left = c.Left; objCtrl.Top = c.Top; objCtrl.Width = c.Width; objCtrl.Height = c.Height;
oldCtrl.Add(objCtrl);
//**放在这里,是先记录控件本身,后记录控件的子控件
if (c.Controls.Count > 0)
AddControl(c);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
}
}
//(3.2)控件自适应大小,
public void controlAutoSize(Control mForm)
{
if (ctrlNo == 0)
{ //*如果在窗体的Form1_Load中,记录控件原始的大小和位置,正常没有问题,但要加入皮肤就会出现问题,因为有些控件如dataGridView的的子控件还没有完成,个数少
//*要在窗体的Form1_SizeChanged中,第一次改变大小时,记录控件原始的大小和位置,这里所有控件的子控件都已经形成
controlRect cR;
// cR.Left = mForm.Left; cR.Top = mForm.Top; cR.Width = mForm.Width; cR.Height = mForm.Height;
cR.Left = 0; cR.Top = 0; cR.Width = mForm.PreferredSize.Width; cR.Height = mForm.PreferredSize.Height;
oldCtrl.Add(cR);//第一个为"窗体本身",只加入一次即可
AddControl(mForm);//窗体内其余控件可能嵌套其它控件(比如panel),故单独抽出以便递归调用
}
float wScale = (float)mForm.Width / (float)oldCtrl[0].Width;//新旧窗体之间的比例,与最早的旧窗体
float hScale = (float)mForm.Height / (float)oldCtrl[0].Height;//.Height;
ctrlNo = 1;//进入=1,第0个为窗体本身,窗体内的控件,从序号1开始
AutoScaleControl(mForm, wScale, hScale);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
}
private void AutoScaleControl(Control ctl, float wScale, float hScale)
{
int ctrLeft0, ctrTop0, ctrWidth0, ctrHeight0;
//int ctrlNo = 1;//第1个是窗体自身的 Left,Top,Width,Height,所以窗体控件从ctrlNo=1开始
foreach (Control c in ctl.Controls)
{ //**放在这里,是先缩放控件的子控件,后缩放控件本身
//if (c.Controls.Count > 0)
// AutoScaleControl(c, wScale, hScale);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
ctrLeft0 = oldCtrl[ctrlNo].Left;
ctrTop0 = oldCtrl[ctrlNo].Top;
ctrWidth0 = oldCtrl[ctrlNo].Width;
ctrHeight0 = oldCtrl[ctrlNo].Height;
//c.Left = (int)((ctrLeft0 - wLeft0) * wScale) + wLeft1;//新旧控件之间的线性比例
//c.Top = (int)((ctrTop0 - wTop0) * h) + wTop1;
c.Left = (int)((ctrLeft0) * wScale);//新旧控件之间的线性比例。控件位置只相对于窗体,所以不能加 + wLeft1
c.Top = (int)((ctrTop0) * hScale);//
c.Width = (int)(ctrWidth0 * wScale);//只与最初的大小相关,所以不能与现在的宽度相乘 (int)(c.Width * w);
c.Height = (int)(ctrHeight0 * hScale);//
ctrlNo++;//累加序号
//**放在这里,是先缩放控件本身,后缩放控件的子控件
if (c.Controls.Count > 0)
AutoScaleControl(c, wScale, hScale);//窗体内其余控件还可能嵌套控件(比如panel),要单独抽出,因为要递归调用
if (ctl is DataGridView)
{
DataGridView dgv = ctl as DataGridView;
Cursor.Current = Cursors.WaitCursor;
int widths = 0;
for (int i = 0; i < dgv.Columns.Count; i++)
{
dgv.AutoResizeColumn(i, DataGridViewAutoSizeColumnMode.AllCells); // 自动调整列宽
widths += dgv.Columns[i].Width; // 计算调整列后单元列的宽度和
}
if (widths >= ctl.Size.Width) // 如果调整列的宽度大于设定列宽
dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells; // 调整列的模式 自动
else
dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; // 如果小于 则填充
Cursor.Current = Cursors.Default;
}
}
}
}
}
2.在要自适应大小的窗体中声明全局类对象
//1.声明自适应类实例
AutoResizeForm asc = new AutoResizeForm();
3.在窗体的Load事件中调用类的初始化方法:
//2.位窗体添加Load时间,并在其中调用类的初始化方法,记录窗体和其控件的初始位置和大小
private void AutoResizeWithResolutionForm_Load(object sender, EventArgs e)
{
asc.controllInitializeSize(this);
}
4.在窗体的SizeChange事件中调用窗体自适应方法:
//3.为窗体添加SizeChanged事件,并在其方法Form1_SizeChanged中,调用类的自适应方法,完成自适应
private void AutoResizeWithResolutionForm_SizeChanged(object sender, EventArgs e)
{
asc.controlAutoSize(this);
}
当然,窗口坐标和屏幕坐标也是可以相互转换的,
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
int x = e.X; //相对form窗口的坐标,客户区坐标
int y = e.Y;
int x1 = Control.MousePosition.X;//相对显示器,屏幕的坐标
int y1 = Control.MousePosition.Y;
}
它们之间转换如下:
this.Location; // 窗体所在坐标
this.PointToScreen(new Point(0, 0)); // 客户区坐标转换为屏幕坐标
this.PointToClient(new Point(0, 0)); // 屏幕坐标转换为客户区坐标