Mobile WinForms Hello World

In the last post, it discussed how to create a simple Hello World example using WinForms.  The example displays the words “Hello World” in the client area of the window and it calculates how big to make the text based on the area available.  The code is not using any Citrix API extensions to make it run properly on a mobile device.

HelloWorld

There a few things that need to change to get it to be mobile friendly.  Here is the list of things that need to be added:

  1. Reference Mobile SDK for Windows Apps Citrix.CMP.dll assembly
  2. Border style changed to none
  3. Add “Using Citrix.Cmp;” at top
  4. Initialize the SDK object and hook certain events
  5. Add conditional code for mobile and non-mobile paths
  6. Whenever the display changes size, be sure to update the text

There are a few more details to understand but these are the main ideas of what needs to change.

The modified code is included here inline to compare against the previous post.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Citrix.Cmp;
using System.Diagnostics;

namespace MobileHelloWorld
{
    /// <summary>
    /// The MobileHelloWorld form demonstrates how to display "Hello World" with the appropriate font based on the size of the client area.
    /// This work was derived from HelloWorld and extended to use the Mobile SDK for Windows Apps
    /// </summary>
    public partial class MobileHelloWorld : Form
    {
        private CmpApi cmpApi = null;
        private bool mobileDevice = false;

        /// <summary>
        /// Main constructor for MobileHelloWorld Form
        /// </summary>
        public MobileHelloWorld()
        {
            // initialize the designer standard components
            InitializeComponent();

            // initialize the Mobile SDK so we can use it
            InitializeMobileSDK();

            // change how the form looks based on what kind of device it is
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;
            }
            else
            {
                // make sure that this window is maximized on non-mobile devices
                WindowState = FormWindowState.Maximized;
            }
        }

        /// <summary>
        /// Initialize the Mobile SDK API object
        /// </summary>
        private void InitializeMobileSDK()
        {
            try
            {
                // create an instance of our API (referenced from Citrix.Cmp.dll assembly)
                cmpApi = new CmpApi();

                // hook the revelant events to support our app

                // viewport is the area we can use for our application
                cmpApi.Display.ViewportInfoChanged += new EventHandler<ViewportInfoChangedArgs>(Display_ViewportInfoChanged);

                // request notification for when we are connected and disconnected to properly handle the mobile device
                cmpApi.SessionManager.SessionConnected += new EventHandler<SessionConnectedArgs>(SessionManager_SessionConnected);
                cmpApi.SessionManager.SessionDisconnected += new EventHandler(SessionManager_SessionDisconnected);

                // the mobile device is only connected whe IsCmpAvailable is true
                if (cmpApi.SessionManager.IsCmpAvailable)
                {
                    mobileDevice = true;
                }
            }
            catch
            {
                // we did not get a working instance of CmpApi.
                // fail gracefully to allow it to work fine on non-mobile devices
            }
        }

        /// <summary>
        /// Session disconnected event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionDisconnected(object sender, EventArgs e)
        {
            // the mobile device is now no longer connected
            mobileDevice = false;
        }

        /// <summary>
        /// Notified when session is connected
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionConnected(object sender, SessionConnectedArgs e)
        {
            // if the mobile device has returned, then allow mobile device calls again
            if (cmpApi.SessionManager.IsCmpAvailable)
            {
                mobileDevice = true;
            }

            // resize the form to match the current connected device
            ResizeForm();
        }

        /// <summary>
        /// Viewport changed event - we are interested in the ClientViewport and use it to resize the main form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Display_ViewportInfoChanged(object sender, ViewportInfoChangedArgs e)
        {
            ViewportInfo viewport = e.NewState;

            if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0.0) && (viewport.ClientViewport.Value.Height != 0.0))
            {
                Size = new Size(Convert.ToInt32(viewport.ClientViewport.Value.Width), Convert.ToInt32(viewport.ClientViewport.Value.Height));

                Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
            }
            else
            {
                if (viewport.ClientViewport.HasValue)
                {
                    Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
                }
                else
                {
                    Trace.WriteLine("ViewportInfoChanged ClientViewport has no value");
                }
            }

            // if the server viewport has changed, make sure our app is at the origin
            if ((viewport.ServerViewport.HasValue) && ((viewport.ServerViewport.Value.X != 0) || (viewport.ServerViewport.Value.Y != 0)))
            {
                Location = new Point(Convert.ToInt32(viewport.ServerViewport.Value.X), Convert.ToInt32(viewport.ServerViewport.Value.Y));
            }
            else
            {
                Location = new Point();
            }
        }

        /// <summary>
        /// Get the current display size for the mobile device.
        /// There are three techniques to attempt in a certain order.
        /// 1. Viewport information
        /// 2. Display state
        /// 3. Actual screen width and height
        /// The viewport is best since it takes into account keyboard and other mobile device areas which are reserved.
        /// The display state is from receiver but does not take into account keyboard or other reserved areas.
        /// The last is the real screen size which comes from Windows.
        /// </summary>
        /// <returns>Size of the display area</returns>
        private Size GetDisplaySize()
        {
            // start with the current size as the default
            Size size = Size;

            if (IsMobileDevice())
            {
                try
                {
                    ViewportInfo viewport = cmpApi.Display.GetViewport();

                    // ViewportInfo.ClientViewport is a nullable value and that means that we have to use HasValue to check it
                    // Also, the iOS Receiver sometimes reports 0,0 we need to check for that too
                    if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0) && (viewport.ClientViewport.Value.Height != 0))
                    {
                        size.Width = Convert.ToInt32(viewport.ClientViewport.Value.Width);
                        size.Height = Convert.ToInt32(viewport.ClientViewport.Value.Height);
                    }
                    else
                    {
                        // The display state resolution is a width, height measurement in pixels
                        DisplayState displayState = cmpApi.Display.GetDisplayState();

                        if (displayState.Resolution.HasValue)
                        {
                            // the display state is the next best place to go
                            size.Width = Convert.ToInt32(displayState.Resolution.Value.Height);
                            size.Height = Convert.ToInt32(displayState.Resolution.Value.Width);
                        }
                        else
                        {
                            // last resort is the screen width and height
                            size.Width = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenWidth);
                            size.Height = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenHeight);
                        }
                    }
                }
                catch
                {
                    // fail gracefully
                }
            }

            return (size);
        }

        /// <summary>
        /// Determines if mobile device is on the other side
        /// </summary>
        /// <returns>true - mobile device, false - non-mobile device</returns>
        private bool IsMobileDevice()
        {
            return (mobileDevice);
        }

        /// <summary>
        /// Called when the form is first loaded.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender">Ignored</param>
        /// <param name="e">Ignored</param>
        private void HelloWorld_Load(object sender, EventArgs e)
        {
            // resize and relocate based on mobile device informtation
            ResizeForm();
        }

        /// <summary>
        /// Resize the Form if it is running on mobile device
        /// </summary>
        private void ResizeForm()
        {
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;

                // Location is always (0,0)
                Location = new Point();

                // Size is calculated from the Mobile SDK information
                Size = GetDisplaySize();
            }
            else
            {
                WindowState = FormWindowState.Maximized;
            }
        }

        /// <summary>
        /// Called when the form is resized.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HelloWorld_Resize(object sender, EventArgs e)
        {
            ResizeHelloWorld();
        }

        /// <summary>
        /// Resize the Hello World to fill the client area
        /// </summary>
        void ResizeHelloWorld()
        {
            // change the font to match the size of the client area
            HelloWorldLabel.Font = GetBestFontFit(ClientSize, HelloWorldLabel.Font, HelloWorldLabel.Text, ClientSize.Height, 8f); ;

            // center the label
            CenterHelloWorld();
        }

        /// <summary>
        /// Center the HelloWorldLabel in the client area of MobileHelloWorld form
        /// </summary>
        void CenterHelloWorld()
        {
            HelloWorldLabel.Location = new Point((ClientSize.Width / 2) - (HelloWorldLabel.Width / 2), (ClientSize.Height / 2) - (HelloWorldLabel.Height / 2));
        }

        /// <summary>
        /// Determine the best font match for the size/text.  The result should be between the maximum and minimum height.
        /// The size is the area to fill.  The new font is based on the old font which is passed in.
        /// </summary>
        /// <returns>Font</returns>
        public static Font GetBestFontFit(Size size, Font currFont, String text, float fontMax, float fontMin)
        {
            float fontPixels = fontMax;
            float minfontPixels = fontMin;

            // find the corresponding font size for the textbox size
            Font font = currFont;

            // make sure that the text is not null or empty before attempting to fit it in the area.
            // if it is null or empty, the current font is used since it does not matter.
            if (!String.IsNullOrEmpty(text))
            {
                Size textsize;

                font = new Font(currFont.FontFamily, fontPixels, currFont.Style, GraphicsUnit.Pixel);

                textsize = TextRenderer.MeasureText(text, font);

                // Check to see if the font fits in the area and if not, find a smaller one
                while ((((textsize.Height) > size.Height) || ((textsize.Width) > size.Width)) && (fontPixels > minfontPixels))
                {
                    // try to speed up the process of finding the right font size by detecting how much it is too big by
                    if (textsize.Height > size.Height)
                    {
                        // reduce the fontPixels based on how far off the font size is
                        fontPixels -= (textsize.Height - size.Height);
                    }
                    else if (textsize.Width > size.Width)
                    {
                        int charPixelWidth = textsize.Width / text.Length;
                        int targetPixelWidth = size.Width / text.Length;

                        // if the characters are too wide, reduce the fontPixels by how different the result is from the desired width
                        if (charPixelWidth > targetPixelWidth)
                        {
                            fontPixels -= (charPixelWidth - targetPixelWidth);
                        }
                        else
                        {
                            fontPixels -= 1.0F;
                        }
                    }
                    else
                    {
                        fontPixels -= 1.0F;
                    }

                    // drop the old one since it did not work
                    font.Dispose();

                    // get a new font based on the smaller font pixels size
                    font = new Font(font.FontFamily, fontPixels, font.Style, GraphicsUnit.Pixel);

                    // recalculate the size based on the new font
                    textsize = TextRenderer.MeasureText(text, font);
                }
            }

            // when all done, return the font to use
            return (font);
        }

    }
}

MobileHelloWorld

The picture above is the result of running the code against the emulator with the iPhone 5 selected. Note that there is no border and that the size matches an iPhone 5 screen.

Now is a good time to break up what the differences are. There are many new lines but these changes are not conceptually very big.

using Citrix.Cmp;

Including a ‘Using’ line just makes it easier to use routines from Citrix.Cmp namespace. It is not required but a “nice to have”.

    /// <summary>
    /// The MobileHelloWorld form demonstrates how to display "Hello World" with the appropriate font based on the size of the client area.
    /// This work was derived from HelloWorld and extended to use the Mobile SDK for Windows Apps
    /// </summary>
    public partial class MobileHelloWorld : Form
    {
        private CmpApi cmpApi = null;
        private bool mobileDevice = false;

        /// <summary>
        /// Main constructor for MobileHelloWorld Form
        /// </summary>
        public MobileHelloWorld()
        {
            // initialize the designer standard components
            InitializeComponent();

            // initialize the Mobile SDK so we can use it
            InitializeMobileSDK();

            // change how the form looks based on what kind of device it is
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;
            }
            else
            {
                // make sure that this window is maximized on non-mobile devices
                WindowState = FormWindowState.Maximized;
            }
        }

The initialization code for the MobileHelloWorld code has been extended to call InitializeMobileSDK and to change the style of the form based on whether or not the device is mobile. The InitializeMobileSDK function is displayed later in this post and is responsible for creating an instance of the SDK object and hooking events. The MobileHelloWorld form is only created once and this related code is only run once. You only need to create the object for the SDK once so this is a good match. Note also that IsMobileDevice is used to decide between maximizing (non-mobile) and getting rid of the border (mobile).

        /// <summary>
        /// Initialize the Mobile SDK API object
        /// </summary>
        private void InitializeMobileSDK()
        {
            try
            {
                // create an instance of our API (referenced from Citrix.Cmp.dll assembly)
                cmpApi = new CmpApi();

                // hook the revelant events to support our app

                // viewport is the area we can use for our application
                cmpApi.Display.ViewportInfoChanged += new EventHandler<ViewportInfoChangedArgs>(Display_ViewportInfoChanged);

                // request notification for when we are connected and disconnected to properly handle the mobile device
                cmpApi.SessionManager.SessionConnected += new EventHandler<SessionConnectedArgs>(SessionManager_SessionConnected);
                cmpApi.SessionManager.SessionDisconnected += new EventHandler(SessionManager_SessionDisconnected);

                // the mobile device is only connected whe IsCmpAvailable is true
                if (cmpApi.SessionManager.IsCmpAvailable)
                {
                    mobileDevice = true;
                }
            }
            catch
            {
                // we did not get a working instance of CmpApi.
                // fail gracefully to allow it to work fine on non-mobile devices
            }
        }

This is InitializeMobileSDK(). The CmpApi object is used for everything in the SDK. In this code, an CmpApi object is created and stored in a cmpApi variable in the form class. Three event handlers are added to the object to be notified of viewport changes and session connect/disconnect. The viewport changes are important for making sure the application gets notified of viewport size changes (usually from screen keyboard or orientation change). The session connect/disconnect events help the application match the state of the device to the application. Beyond creating the CmpApi object, the function also makes sure that a mobile device is actually there. If it is, it then sets the mobileDevice flag which is used with IsMobileDevice function. If anything fails during this function call with an exception, the try/catch section will prevent the exception from being passed up. This is done to make the application function without exceptions if not connected to a mobile device.

        /// <summary>
        /// Session disconnected event
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionDisconnected(object sender, EventArgs e)
        {
            // the mobile device is now no longer connected
            mobileDevice = false;
        }

        /// <summary>
        /// Notified when session is connected
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void SessionManager_SessionConnected(object sender, SessionConnectedArgs e)
        {
            // if the mobile device has returned, then allow mobile device calls again
            if (cmpApi.SessionManager.IsCmpAvailable)
            {
                mobileDevice = true;
            }

            // resize the form to match the current connected device
            ResizeForm();
        }

Session connect/disconnect events correspond to when the session is first connected to the device and when the session is disconnected. The disconnect event is only needed to know that the mobile device is no longer connected. The connect event is used to check for a mobile device and to resize the form to match the screen size.

        /// <summary>
        /// Viewport changed event - we are interested in the ClientViewport and use it to resize the main form
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Display_ViewportInfoChanged(object sender, ViewportInfoChangedArgs e)
        {
            ViewportInfo viewport = e.NewState;

            if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0.0) && (viewport.ClientViewport.Value.Height != 0.0))
            {
                Size = new Size(Convert.ToInt32(viewport.ClientViewport.Value.Width), Convert.ToInt32(viewport.ClientViewport.Value.Height));

                Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
            }
            else
            {
                if (viewport.ClientViewport.HasValue)
                {
                    Trace.WriteLine(String.Format("ViewportInfoChanged Rect ClientViewport {0}", viewport.ClientViewport.Value));
                }
                else
                {
                    Trace.WriteLine("ViewportInfoChanged ClientViewport has no value");
                }
            }

            // if the server viewport has changed, make sure our app is at the origin
            if ((viewport.ServerViewport.HasValue) && ((viewport.ServerViewport.Value.X != 0) || (viewport.ServerViewport.Value.Y != 0)))
            {
                Location = new Point(Convert.ToInt32(viewport.ServerViewport.Value.X), Convert.ToInt32(viewport.ServerViewport.Value.Y));
            }
            else
            {
                Location = new Point();
            }
        }

The viewport event is very important for knowing the correct form size. The event has both client and server viewport information. The client viewport reveals the width and height of the area on the mobile device that can be used. The server viewport reveals what section of the server display is currently visible. Usually the two viewports match up. However, there are times when they do not. The most obvious cases are when the screen keyboard is displayed or when zooming is used. The event handler above is using tracing since viewport events can be a bit tricky to understand. I added the trace to understand why it was originally not doing what I wanted when using a keyboard. During the development of the SDK, it has been very helpful to understand the various events and what the values can be in different states. The event handler is using the client viewport to resize the form. It also uses the server viewport to reposition the form. Between these two sources of information, the form is correctly sized and positioned.

        /// <summary>
        /// Get the current display size for the mobile device.
        /// There are three techniques to attempt in a certain order.
        /// 1. Viewport information
        /// 2. Display state
        /// 3. Actual screen width and height
        /// The viewport is best since it takes into account keyboard and other mobile device areas which are reserved.
        /// The display state is from receiver but does not take into account keyboard or other reserved areas.
        /// The last is the real screen size which comes from Windows.
        /// </summary>
        /// <returns>Size of the display area</returns>
        private Size GetDisplaySize()
        {
            // start with the current size as the default
            Size size = Size;

            if (IsMobileDevice())
            {
                try
                {
                    ViewportInfo viewport = cmpApi.Display.GetViewport();

                    // ViewportInfo.ClientViewport is a nullable value and that means that we have to use HasValue to check it
                    // Also, the iOS Receiver sometimes reports 0,0 we need to check for that too
                    if ((viewport.ClientViewport.HasValue) && (viewport.ClientViewport.Value.Width != 0) && (viewport.ClientViewport.Value.Height != 0))
                    {
                        size.Width = Convert.ToInt32(viewport.ClientViewport.Value.Width);
                        size.Height = Convert.ToInt32(viewport.ClientViewport.Value.Height);
                    }
                    else
                    {
                        // The display state resolution is a width, height measurement in pixels
                        DisplayState displayState = cmpApi.Display.GetDisplayState();

                        if (displayState.Resolution.HasValue)
                        {
                            // the display state is the next best place to go
                            size.Width = Convert.ToInt32(displayState.Resolution.Value.Height);
                            size.Height = Convert.ToInt32(displayState.Resolution.Value.Width);
                        }
                        else
                        {
                            // last resort is the screen width and height
                            size.Width = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenWidth);
                            size.Height = Convert.ToInt32(System.Windows.SystemParameters.PrimaryScreenHeight);
                        }
                    }
                }
                catch
                {
                    // fail gracefully
                }
            }

            return (size);
        }

Being able to get the correct display size is very important to the application. In order to make this more fool-proof, the GetDisplaySize function uses three different techniques to get the information. In priority order, the different sources are used to find the right match. Having a fall-back makes sure that it will always get the best answer in different environments. It is also implemented to gracefully fail and not change the size if all these techniques fail. The main source will always be coming from viewport client viewport.

        /// <summary>
        /// Determines if mobile device is on the other side
        /// </summary>
        /// <returns>true - mobile device, false - non-mobile device</returns>
        private bool IsMobileDevice()
        {
            return (mobileDevice);
        }

Instead of querying the mobile device all the time, the status of the mobile device connection is kept in a boolean that is used in a function called IsMobileDevice.

        /// <summary>
        /// Called when the form is first loaded.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender">Ignored</param>
        /// <param name="e">Ignored</param>
        private void HelloWorld_Load(object sender, EventArgs e)
        {
            // resize and relocate based on mobile device informtation
            ResizeForm();
        }

        /// <summary>
        /// Resize the Form if it is running on mobile device
        /// </summary>
        private void ResizeForm()
        {
            if (IsMobileDevice())
            {
                // do not use the resizable border on mobile devices
                FormBorderStyle = FormBorderStyle.None;

                // Location is always (0,0)
                Location = new Point();

                // Size is calculated from the Mobile SDK information
                Size = GetDisplaySize();
            }
            else
            {
                WindowState = FormWindowState.Maximized;
            }
        }
        /// <summary>
        /// Called when the form is resized.  Resize the HelloWorldLabel to fit the new form size
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HelloWorld_Resize(object sender, EventArgs e)
        {
            ResizeHelloWorld();
        }

        /// <summary>
        /// Resize the Hello World to fill the client area
        /// </summary>
        void ResizeHelloWorld()
        {
            // change the font to match the size of the client area
            HelloWorldLabel.Font = GetBestFontFit(ClientSize, HelloWorldLabel.Font, HelloWorldLabel.Text, ClientSize.Height, 8f);

            // center the label
            CenterHelloWorld();
        }

Whenever the form is first loaded or when the session is reconnected, it calls ResizeForm. The ResizeForm then uses GetDisplaySize to resize the actual form size. When the form is resized, it triggers the form resize event when then resizes the text and centers it. The chain of events makes sure that a form resize will lead to a text resize as well. The code for resizing the text is the same as before.

There are things that might help to reduce the number of lines being used to make it a mobile app but I do think that it is more honest to show what the details are. The MobileHelloWorld.cs source file is 343 lines on my development machine. Also keep in mind that the infrastructure of using the SDK is lightweight due to using the object model and the fact that it does not require much to use the API. The original HelloWorld example was 138 lines. That means about 200 lines were added to support a mobile device. Also keep in mind that this is not lines of code but rather actual text file lines include brackets and blank lines. Also keep in mind that I wrote the two examples on the same day and only postponed this example till today since I needed to do a blog post for it. In other words, it took longer to do the post than it did to modify the HelloWorld program to support a mobile device display.

If you would a copy of the source for the entire project, it is available on ShareFile. This time I did not include the binary since it might be considered untrustworthy by virus scanners. If you have any questions, please let me know using the comments.

About

Live near Brisbane, Australia. Software developer currently focused on iOS and Android. Avid Google Local Guide

Tagged with: , , , ,
Posted in Citrix SDK, Mobile SDK for Windows Apps, XenApp, XenDesktop
4 comments on “Mobile WinForms Hello World
  1. ch says:

    Hello, I would like to know whether Mobile SDK for Windows Apps would work on VB6 applications?…. Thanks.

    • jeffreymuir says:

      Mobile SDK for Windows Apps does not support VB6 applications. It was an early goal to support them but later we determined that it was too difficult to do with all the VB6 restrictions with COM automation. We do support using VB.NET which is very similar and is seen as the next generation of VB technology.

  2. ch says:

    Thank you for clarifying this Jeff.

Comments are closed.

Archives
Categories
Follow Red Circle Blog on WordPress.com
%d bloggers like this: