Thursday, July 30, 2009

Developing Orientation-Aware and Dpi-Aware Smart Device Application in .NET

When developing smart device application, we need to be aware of screen orientation and DPI(Dots Per Inch) since 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.

You can also read this article and download the source code from Code Project web site.

The following screen shot shows this application in 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 the button disappeared. You have to scroll the screen to see it. That’s probably not a good idea for the user who prefer landscape screen.

clip_image002clip_image004

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 Form’s Resize event, so it will change 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.



clip_image006



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.



clip_image008clip_image010



It doesn’t look right, does it? Yes, that is because 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 displays everything correctly across different devices.



You may be able to use “Dock” property to achieve some degree of flexibility. But when you have complicate 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 controls’ location and size based on device’s DPI. Since we set the location and size in the Portrait() and Landscape() methods, we need to scale those settings after calling those methods. Below is the modified version of 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 screen shot, everything is back to normal.



clip_image012 clip_image014



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 implement 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 IRotatable interface too.





public partial class Form2 : Form, IRotatable





Add 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 what it looks like with a user control.



clip_image016clip_image018




No comments:

Post a Comment