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.
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.
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.
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:
public class DpiHelper
{
private static int dpi =
SafeNativeMethods.GetDeviceCaps(IntPtr.Zero, 88);
public static bool IsRegularDpi
{
get
{
if (dpi == 96) return true;
else return false;
}
}
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:
break;
}
}
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.
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:
Happy programming!