Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Developing Orientation-Aware and DPI-Aware Smart Device Applications in .NET

0.00/5 (No votes)
29 Jul 2009 1  
This article will show you how to create an Orientation-Aware and DPI-Aware Smart Device application.

Introduction

When developing Smart Device applications, we need to be aware of screen orientation and DPI (Dots Per Inch) since the user’s device may vary. In this article, I am going to use a simple application to demonstrate how to create an Orientation-Aware and DPI-Aware Smart Device application.

Using the code

The following screenshot shows this application in the Windows Mobile 6 Classic Emulator. It has some labels, textboxes, and a button. But, when you change the screen orientation to landscape, you will see that the button has disappeared. You have to scroll the screen to see it. That’s probably not a good idea for the user who prefers a landscape screen.

DpiAwareApp/image002.jpgDpiAwareApp/image004.jpg

There is no easy way to solve this issue. If your form is not that crowded, you can try to squeeze everything into the upper half of the form. But in case you do have a lot of controls to display, one way to solve this issue is to create a landscape view of the form and dynamically re-position all the controls based on the screen orientation.

Here, we first create the portrait view, copy the code regarding position and size, and create a method called Portrait(). Then, we rotate the design view to create the landscape view and also create a method called 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);
}

Add the following code to the Form’s Resize event, so it will change the layout when the screen orientation is changed. Here, we use Screen.PrimaryScreen.Bounds to determine the orientation.

void Form1_Resize(object sender, EventArgs e)
{
    if (Screen.PrimaryScreen.Bounds.Height > 
        Screen.PrimaryScreen.Bounds.Width) Portrait();
    else Landscape();
}

Now, it looks nice in landscape orientation too.

DpiAwareApp/image006.jpg

However, this solution works just fine until you have to run this application on a higher DPI device. Now, let us change our emulator to Windows Mobile 6.1.4 Professional – VGA, which is 480x640.

DpiAwareApp/image008.jpgDpiAwareApp/image010.jpg

It doesn’t look right, does it? No, that is because of the two methods we added. If the actual device’s DPI is different from our designer, we cannot set the absolute position in our code and expect it to display everything correctly across different devices.

You may be able to use the “Dock” property to achieve some degree of flexibility. But when you have a complicated layout, you may end up adding too many panels, and it is not an easy thing to do.

I found this article about creating a DpiHelper for .NET CF 1.1. It provided a way to add High-DPI support programmatically. I borrowed its idea and used it in this application to adjust control locations and size based on the device’s DPI. Since we set the location and size in the Portrait() and Landscape() methods, we need to scale the settings after calling those methods. Below is the modified version of the DpiHelper class:

/// <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);
    }
}

And here is how to use it:

void Form1_Resize(object sender, EventArgs e)
{
    if (Screen.PrimaryScreen.Bounds.Height > 
        Screen.PrimaryScreen.Bounds.Width) Portrait();
    else Landscape();
    DpiHelper.AdjustAllControls(this);
}

As you can see in the following screenshot, everything is back to normal.

DpiAwareApp/image012.jpgDpiAwareApp/image014.jpg

If you use user controls in your form, you may want to modify the code to make it more generic.

First, create an interface called IRotatable.

interface IRotatable
{
    void Portrait();
    void Landscape();
}

Second, create the user control which implements the IRotatable interface.

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
}

Then, modify the form to implement the IRotatable interface too.

public partial class Form2 : Form, IRotatable

Add a new method to recursively loop though the form and all the controls in it.

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);
    }
}

This is the form with a user control in it:

DpiAwareApp/image016.jpgDpiAwareApp/image018.jpg

Happy programming!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here