在 .NET 中开发方向感知和 DPI 感知的智能设备应用程序






4.08/5 (7投票s)
本文将向您展示如何创建方向感知和 DPI 感知的智能设备应用程序。
引言
在开发智能设备应用程序时,我们需要注意屏幕方向和 DPI(每英寸点数),因为用户的设备可能有所不同。 在本文中,我将使用一个简单的应用程序来演示如何创建方向感知和 DPI 感知的智能设备应用程序。
使用代码
以下屏幕截图显示了 Windows Mobile 6 Classic 模拟器中的此应用程序。 它有一些标签、文本框和一个按钮。 但是,当您将屏幕方向更改为横向时,您会发现该按钮消失了。 您必须滚动屏幕才能看到它。 对于喜欢横向屏幕的用户来说,这可能不是一个好主意。


没有简单的办法解决这个问题。 如果您的表单不太拥挤,您可以尝试将所有内容挤压到表单的上半部分。 但是,如果您确实有很多控件要显示,解决此问题的一种方法是创建表单的横向视图,并根据屏幕方向动态地重新定位所有控件。
在这里,我们首先创建纵向视图,复制有关位置和大小的代码,并创建一个名为 Portrait() 的方法。 然后,我们旋转设计视图以创建横向视图,并创建一个名为 Landscape() 的方法。
public void Portrait()
{
    this.SuspendLayout();
    this.button2.Location = new System.Drawing.Point(81, 232);
    this.button2.Size = new System.Drawing.Size(72, 20);
    this.label0.Location = new System.Drawing.Point(45, 9);
    this.label0.Size = new System.Drawing.Size(141, 20);
    this.textBox1.Location = new System.Drawing.Point(111, 32);
    this.textBox1.Size = new System.Drawing.Size(100, 21);
    this.label1.Location = new System.Drawing.Point(16, 33);
    this.label1.Size = new System.Drawing.Size(74, 20);
    this.label2.Location = new System.Drawing.Point(16, 70);
    this.label2.Size = new System.Drawing.Size(74, 20);
    this.textBox2.Location = new System.Drawing.Point(111, 69);
    this.textBox2.Size = new System.Drawing.Size(100, 21);
    this.ResumeLayout(false);
}
public void Landscape()
{
    this.SuspendLayout();
    this.button2.Location = new System.Drawing.Point(132, 152);
    this.button2.Size = new System.Drawing.Size(72, 20);
    this.label0.Location = new System.Drawing.Point(101, 10);
    this.label0.Size = new System.Drawing.Size(141, 20);
    this.textBox1.Location = new System.Drawing.Point(52, 39);
    this.textBox1.Size = new System.Drawing.Size(100, 21);
    this.label1.Location = new System.Drawing.Point(3, 40);
    this.label1.Size = new System.Drawing.Size(43, 20);
    this.label2.Location = new System.Drawing.Point(173, 40);
    this.label2.Size = new System.Drawing.Size(54, 20);
    this.textBox2.Location = new System.Drawing.Point(233, 39);
    this.textBox2.Size = new System.Drawing.Size(100, 21);
    this.ResumeLayout(false);
}
将以下代码添加到 Form 的 Resize 事件中,以便在屏幕方向更改时更改布局。 在这里,我们使用 Screen.PrimaryScreen.Bounds 来确定方向。
void Form1_Resize(object sender, EventArgs e)
{
    if (Screen.PrimaryScreen.Bounds.Height > 
        Screen.PrimaryScreen.Bounds.Width) Portrait();
    else Landscape();
}
现在,在横向方向上也看起来不错。

但是,此解决方案只有在您必须在更高的 DPI 设备上运行此应用程序时才能正常工作。 现在,让我们将我们的模拟器更改为 Windows Mobile 6.1.4 Professional – VGA,即 480x640。


看起来不太对劲,不是吗? 不,这是因为我们添加了两个方法。 如果实际设备的 DPI 与我们的设计器不同,我们无法在代码中设置绝对位置,并期望它在不同的设备上正确显示所有内容。
您可以使用“Dock”属性来实现一定程度的灵活性。 但是,当您有一个复杂的布局时,您最终可能会添加太多的面板,而且这不是一件容易的事情。
我发现了这篇关于为 .NET CF 1.1 创建 DpiHelper 的文章。 它提供了一种以编程方式添加高 DPI 支持的方法。 我借鉴了它的想法,并将其用于此应用程序中,以根据设备的 DPI 调整控件位置和大小。 由于我们在 Portrait() 和 Landscape() 方法中设置了位置和大小,因此我们需要在调用这些方法后缩放设置。 以下是 DpiHelper 类的修改版本
/// <summary>A helper object to adjust the sizes of controls based on the DPI.</summary>
public class DpiHelper
{
    /// <summary>The real dpi of the device.</summary>
    private static int dpi = 
      SafeNativeMethods.GetDeviceCaps(IntPtr.Zero, /*LOGPIXELSX*/88);
    public static bool IsRegularDpi
    {
        get
        {
            if (dpi == 96) return true;
            else return false;
        }
    }
    /// <summary />Adjust the sizes of controls to account
    ///           for the DPI of the device.</summary >
    /// <param name="parent" />The parent node
    ///          of the tree of controls to adjust.</param />
    public static void AdjustAllControls(Control parent)
    {
        if (!IsRegularDpi)
        {
            foreach (Control child in parent.Controls)
            {
                AdjustControl(child);
                AdjustAllControls(child);
            }
        }
    }
    public static void AdjustControl(Control control)
    {
        if (control.GetType() == typeof(TabPage)) return;
        switch (control.Dock)
        {
            case DockStyle.None:
                control.Bounds = new Rectangle(
                    control.Left * dpi / 96,
                    control.Top * dpi / 96,
                    control.Width * dpi / 96,
                    control.Height * dpi / 96);
                break;
            case DockStyle.Left:
            case DockStyle.Right:
                control.Bounds = new Rectangle(
                    control.Left,
                    control.Top,
                    control.Width * dpi / 96,
                    control.Height);
                break;
            case DockStyle.Top:
            case DockStyle.Bottom:
                control.Bounds = new Rectangle(
                    control.Left,
                    control.Top,
                    control.Width,
                    control.Height * dpi / 96);
                break;
            case DockStyle.Fill:
                //Do nothing;
                break;
        }
    }
    /// <summary />Scale a coordinate to account for the dpi.</summary />
    /// <param name="x" />The number of pixels at 96dpi.</param />
    public static int Scale(int x)
    {
        return x * dpi / 96;
    }
    public static int UnScale(int x)
    {
        return x * 96 / dpi;
    }
    private class SafeNativeMethods
    {
        [DllImport("coredll.dll")]
        static internal extern int GetDeviceCaps(IntPtr hdc, int nIndex);
    }
}
这是它的用法
void Form1_Resize(object sender, EventArgs e)
{
    if (Screen.PrimaryScreen.Bounds.Height > 
        Screen.PrimaryScreen.Bounds.Width) Portrait();
    else Landscape();
    DpiHelper.AdjustAllControls(this);
}
正如您在以下屏幕截图中看到的,一切都恢复正常了。


如果您在表单中使用用户控件,您可能需要修改代码以使其更通用。
首先,创建一个名为 IRotatable 的接口。
interface IRotatable
{
    void Portrait();
    void Landscape();
}
其次,创建实现 IRotatable 接口的用户控件。
public partial class UserControl1 : UserControl, IRotatable
{
    public UserControl1()
    {
        InitializeComponent();
    }
    #region IRotatable Members
    public void Portrait()
    {
        this.SuspendLayout();
        this.label1.Location = new System.Drawing.Point(18, 13);
        this.label1.Size = new System.Drawing.Size(100, 20);
        this.checkBox1.Location = new System.Drawing.Point(43, 36);
        this.checkBox1.Size = new System.Drawing.Size(100, 20);
        this.button1.Location = new System.Drawing.Point(57, 96);
        this.button1.Size = new System.Drawing.Size(72, 20);
        this.checkBox2.Location = new System.Drawing.Point(43, 63);
        this.checkBox2.Size = new System.Drawing.Size(100, 20);
        this.Size = new System.Drawing.Size(210, 134);
        this.ResumeLayout(false);
    }
    public void Landscape()
    {
        this.SuspendLayout();
        this.label1.Location = new System.Drawing.Point(49, 15);
        this.label1.Size = new System.Drawing.Size(100, 20);
        this.checkBox1.Location = new System.Drawing.Point(1, 38);
        this.checkBox1.Size = new System.Drawing.Size(100, 20);
        this.button1.Location = new System.Drawing.Point(62, 64);
        this.button1.Size = new System.Drawing.Size(72, 20);
        this.checkBox2.Location = new System.Drawing.Point(107, 38);
        this.checkBox2.Size = new System.Drawing.Size(100, 20);
        this.Size = new System.Drawing.Size(210, 98);
        this.ResumeLayout(false);
    }
    #endregion
}
然后,修改表单以也实现 IRotatable 接口。
public partial class Form2 : Form, IRotatable
添加一个新方法以递归地循环遍历表单及其中的所有控件。
void Form2_Resize(object sender, EventArgs e)
{
    SetControlLocation(this);
}
private void SetControlLocation(Control control)
{
    if (control is IRotatable)
    {
        IRotatable rotatableControl = (IRotatable)control;
        if (Screen.PrimaryScreen.Bounds.Height > 
            Screen.PrimaryScreen.Bounds.Width) rotatableControl.Portrait();
        else rotatableControl.Landscape();
    }
    DpiHelper.AdjustControl(control);
    foreach (Control child in control.Controls)
    {
        SetControlLocation(child);
    }
}
这是包含用户控件的表单


编程愉快!


