Advanced User Interfaces with the .NET Compact Framework The .NET Compact Framework offers some advanced features that will be of interest to developers. Because these concepts are specific to the .NET Compact Framework, they are specifically drawn out in this section. Other mobile device runtime frameworks may have similar concepts, so this may be worth reading even if you are using one of these.Dynamic Creation of Controls It can be useful to be able to dynamically create controls at runtime. As shown in Figure 13.8 and Listing 13.2, this is a simple thing to do with the .NET Compact Framework. Dynamic controls are useful to use for several reasons:When form load time is lagging When there are a lot of controls on a form or the controls are sufficiently heavyweight, it can take a significant amount of processing to initialize them. All controls you lay out on your design surface are initialized when the form is created and loaded. This initialization takes place in the function called InitializeComponent(); the contents of this function are managed by the form's designer; you can examine the contents of this function if you want. If you want to speed up the form's load time, you may want to defer the creation of a control until it is needed. Note: You may also choose to take the autogenerated code in the InitializeComponent() function and attempt to hand-optimize it. If you do this, you should place your own code into a separately named function so that it does not get accidentally overwritten by the development tool. You should also be aware that the Form designer may no longer be available to help you design your form. As with any optimization, be sure to measure rigorously to make sure you are getting the optimizations you expect!When the number of controls you will need is unknown at design time For example, if your mobile application needs an array of RadioButton controls whose number is based on the number of items returned from a database query, you will need to create this array at runtime when the number of needed controls is determined.When no design-time implementation exists for the control you want to use For the .NET Compact Framework, it can be a significant challenge to create design-time instances of custom controls; often this can take more development time than the runtime versions of the controls. If you are creating a custom control for your own development use, it may not be worth the trouble of creating a design-time version of it just so it can appear in the Form designer. Instead you can save the unneeded development effort and just create the control at runtime.
Figure 13.8. Dynamic creation of controls at runtime. [View full size image] There are three things you need to do after you create an instance of a dynamic control to make it work:Initialize the control. You should set the size and position of the control and any other properties that need to be set before you display the control.Hook up any events you want to handle. Most controls are only useful when code is hooked up to handle events they can generate. For any events you want to hook up, there are several steps to take:You will need a function that is the event sink (that is, gets called when the event is triggered).You will need to create an event handler (a.k.a. a delegate with the right function signature) that points to your event sink function.You will need to register this event handler with the control.This may sound complicated, but in practice it is usually simple and can be accomplished in one line of code. For example, in this code, a., b., and c. occur: newButton.Click += new System.EventHandler (this.ClickHandlerForButtons);
They are:this.ClickHandlerForButtons is the event sink function.new System.EventHandler() is the delegate that points to the event sink.newButton.Click +=... adds the event handler to the list of event handlers that get called when the event gets fired.Looking in a Form's InitializeComponent() function can be a good place to copy event handler registration code from. It is important to note that a single function can be an event sink for an arbitrary number of different events. This is very useful for working with arrays of controls because the events for multiple controls can be mapped to a single function.Set the parent property of the new control to the form it needs to be displayed on. This final step really creates and hosts the control on the form. As long as the control's parent property points to null, the control is not on the form.Dynamic Control Creation Example Figure 13.8 and Listing 13.2 show a sample application that dynamically creates Button controls and hooks up Click event handlers to these controls. The example can easily be adapted to dynamically create any kind of control you want.The code in Listing 13.2 belongs inside a form in a Pocket PC project. Do the following to build and run the application:
1. | Start Visual Studio .NET (2003 or later) and create a C# Smart Device Application. | 2. | Choose Pocket PC as the target platform. (A project will be created for you and Pocket PC Form designer will appear.) | 3. | Add a Button control to the form and rename it buttonCreateNewButtons. | 4. | Double-click the button you just added to the Form designer. The code window will pop up with a function skeleton for private void buttonCreateNewButtons_Click(object sender,System.EventArgs e). Enter in the code listed below for this function. | 5. | Into the code window, enter in the rest of the code listed below, both the code above and below the listing of the function you just entered. | 6. | Set the MinimizeBox property of the form to false. At runtime this will give the form an OK box at the top-right corner that makes it easy to close the form and exit the application. This is very useful for repeated testing. | 7. | Run the application. You should observe that each time you click the buttonCreateNewButtons button, a new button is added to the form (as shown in Figure 13.8). Clicking any of the new buttons should trigger the event handler code listed below and show a message box that shows the text of the control that was clicked. |
Listing 13.2. Dynamic Creation of Controls on a Form at Runtime
//--------------------------------------------------------------- //Counter for the number of button controls we create //--------------------------------------------------------------- private int m_nextNewButtonIndex; //------------------------------------------------------------- //EVENT HANDLER: Click event handler for the button we have // on our form. // //This function creates a new button, attaches it to our //form and hooks up a "Click" event handler for it //------------------------------------------------------------- private void buttonCreateNewButtons_Click(object sender, System.EventArgs e) { //Eventually we will start creating new buttons off the //bottom of the screen, so lets stop at 8 if (m_nextNewButtonIndex > 8) { return; } //----------------------------------------------------- //Create the button (not yet attached to our form) //set its location, size, and text //----------------------------------------------------- const int newButtonHeight = 15; System.Windows.Forms.Button newButton; newButton = new System.Windows.Forms.Button(); newButton.Width = 100; newButton.Height = newButtonHeight; newButton.Left = 2; newButton.Top = (newButtonHeight + 2) * m_nextNewButtonIndex ; newButton.Text = "New Button " + m_nextNewButtonIndex.ToString(); //----------------------------------------------------- //Attach an event handler to the "Click" event of this //control. //----------------------------------------------------- newButton.Click += new System.EventHandler (this.ClickHandlerForButtons); //----------------------------------------------------- //Attach this button to the form. This will actually //Create the button on the form! //----------------------------------------------------- newButton.Parent = this; //Increment our counter for the next button we create m_nextNewButtonIndex++; } //--------------------------------------------------------- //Event handler we will dynamically hook up to our new //buttons //--------------------------------------------------------- private void ClickHandlerForButtons(object sender, System.EventArgs e) { Button buttonCausingEvent; buttonCausingEvent = (System.Windows.Forms.Button) sender; //Bring up a message box announcing that we have received //the event System.Windows.Forms.MessageBox.Show( "Click event from: \n\r'" + buttonCausingEvent.Text + "'"); }
Custom Controls and Overriding Behaviors in Existing Controls The .NET Compact Framework allows two kinds of control inheritance: (1) building a custom control from scratch, and (2) overriding the nonpainting/nonrendering behaviors of existing System.Windows.Forms.* controls.First a few words on what the .NET Compact Framework (version 1.1) does not support: Unlike the desktop .NET Framework, the .NET Compact Framework does not allow developers to override the visual characteristics of how the standard controls are drawn. (For example, you cannot inherit from a Button, TreeView, TextBox, or other standard control and override how it is painted.) This is for internal performance reasons.A developer wanting a control with a custom look on the .NET Compact Framework will have to derive from the base Control class (System.Windows.Forms.Control), which does allow for the custom rendering of a control. This is most useful for controls that produce completely new user interactions (for example, a charting control) rather than controls that give modified behaviors for existing controls. A basic example for creating a custom rendering control from scratch is shown in Chapter 11, "Graphics and User Interface Performance"; this would be a good place to start if you want to create your own custom rendering control.Although the .NET Compact Framework does not support overriding the rendering behavior of its internal controls it does support overriding the functional behavior of these controls. There are two ways to extend the internal controls: (1) adding additional methods, properties, and events that expose higher-level value-added functionality; and (2) overriding existing properties and methods to supply a more purpose-dedicated experience. A good example showing both of these characteristics is creating a filtered TextBox control that only accepts certain formats of input. The sample code in Listing 13.3 and Listing 13.4 does this.Filtered Text Box Example When input needs to meet specific formatting rules, it is often useful to create a custom control that explicitly forces the input to meet this criteria. A common example in the United States is the need to enter a Social Security number. Social Security numbers come in the format of ###-##-####, three digits separated by a dash, followed by two digits separated by a dash, and finally four more digits. There are many additional other text-input formats it may be useful to enforce, another example being postal codes (Zip codes). Different countries use different formats, some numeric and some alphanumeric; for example, both Canadian and United Kingdom postal codes consist of both numbers and letters. In all of these cases, if precise input is required, a filtering TextBox control can be a valuable asset. It would also be useful for this control to have a property that tells developers whether the text currently entered meets the definition of valid and complete input. Our sample code will do both filtering and validation.Figure 13.9 shows the test application at runtime. The button on the form is pressed to create an instance of the filtering TextBox control. Typing into the text box causes the filtering and formatting code in our SocialSecurityTextBox class to get run. Two functions are called in this class:The first function that is called is the overridden SocialSecurityTextBox.OnKeyPress() method. This gives us a chance to intercept and prefilter the key press events that come in. In our case, because we do not want any letters to be input, we will filter these out if the user tries to enter them. By not passing them on to the base class implementation of OnKeyPress(), we are preventing the text box from ever seeing these key presses. We could do much more filtering and do things such as disallowing any additional numeric input if the user tries to enter more digits after the end of the number, but for the example let's keep things simple. It is worth noting that care should be taken in filtering key events so as not to be overly restrictive and filter out useful key press events such as the backspace character that is used to delete text.The second function that is called is the overridden Social-SecurityTextBox.OnTextChanged() method. This is called when the contents of the Text property change, such as when a key press has been registered. Here we take the opportunity to apply our formatting code and force any text that has been entered into meeting our defined format. We keep only the numbers that have been entered and make sure that there is a dash (-) separating the third and forth digit and the fifth and sixth digit. Care must be taken here as well because if we update the Text property of the text box inside the OnTextChanged method this will cause our OnTextChanged method to be called recursively. We do not want to get into a complicated situation here, so we check for this case at the beginning of the function and exit doing nothing if this occurs. Next we check the length of our processed text; if it is 11 characters, we have a full Social Security number, if not, we do not. We update our internal state to indicate this. Finally we call our text box base class' OnTextChanged() method; this causes any event handlers that are listening for TextChange events to get called.Figure 13.9. Dynamic creation of controls at runtime. [View full size image] The code in Listing 13.3 is a standalone class and can be entered as is. The code in Listing 13.4 belongs inside a form in a Pocket PC project. Do the following to build and run the application:
1. | Start Visual Studio .NET (2003 or later) and create a C# Smart Device Application. | 2. | Choose Pocket PC as the target platform. (A project will be created for you and Pocket PC Form designer will appear.) | 3. | Add a Button control to the form. (It will be named button1.) | 4. | Add a Label control to the form. (It will be named label1.) | 5. | Add a new class to the project. Name the class SocialSecurityTextBox, delete all preexisting code that is shown in the text editor for this class, and enter the code in Listing 13.3. | 6. | Go back to Form1's designer. | 7. | Double-click the button you added to the Form designer. The code window will pop up with a function skeleton for private void button1_Click(object sender, System.EventArgs e).Enter in the code listed in Listing 13.4 for this function. | 8. | Into the code window, enter in the rest of the code listed below, both the code above and below the listing of function you just entered. | 9. | Set the MinimizeBox property of the form to false. At runtime this will give the form an OK box at the top-right corner that makes it easy to close the form and exit the application. This is very useful for repeated testing. | 10. | Run the application. You should observe that when you click the button1 button, a new text box is added to the top of the form. The text box enables you to type only numbers into it and formats the numbers into the template ###-##-####. As you type, the label on the screen is updated to tell you whether you have entered all the numbers required. |
This example could easily be updated to support other input formats. In addition, support for custom events could be added; for instance, our inherited TextBox control could raise an event when all the necessary data to meet an input template was entered.Listing 13.3. A Filtering Text Box That Takes the Format ###-##-####
using System; //-------------------------------------------------------------- //This class is a control derived from the TextBox control. //It uses all the drawing behavior of the TextBox, but adds //a filter on the text box contents ensuring that only text //that meets the format: // ###-##-#### //can be entered. This matches the format of Social Security //numbers used in the United States. //-------------------------------------------------------------- public class SocialSecurityTextBox : System.Windows.Forms.TextBox { private bool m_inputIsFullValidEntry; //------------------------------------------------------------- //Indicates whether we have a full //Social Security number //------------------------------------------------------------- public bool IsFullValidInput {get {return m_inputIsFullValidEntry;}} //A string builder we will use often System.Text.StringBuilder m_sb; //The maximum length of our processed string const int SSNumberLength = 11; //------------------------------------------------------------- //Constructor //------------------------------------------------------------- public SocialSecurityTextBox() { //Allocate our string builder and give us a few extra //characters of room to work in by default m_sb = new System.Text.StringBuilder(SSNumberLength + 5); m_inputIsFullValidEntry = false; } //------------------------------------------------------------- //Format incoming text to make sure it is in the format: // // SS Format : ###-##-#### // char index: 01234567890 // // [in] inString : Text we want to format // [in/out] selectionStart: Current insert point in the text, // this will get moved if it needs to // based on appends or deletes we make //------------------------------------------------------------- private string formatText_NNN_NN_NNNN(string inString, ref int selectionStart) { const int firstDashIndex = 3; const int secondDashIndex = 6; //Clear out the old data, and place the input string into //the string builder so we can work on it. m_sb.Length = 0; m_sb.Append(inString); //--------------------------------------------------- //Go through each character in the string up until //we reach the maximum length of our formatted text //--------------------------------------------------- int currentCharIndex; currentCharIndex = 0; while((currentCharIndex < m_sb.Length) && (currentCharIndex < SSNumberLength)) { char currentChar; currentChar = m_sb[currentCharIndex]; if((currentCharIndex == firstDashIndex) || (currentCharIndex == secondDashIndex)) //------------------------------------------------------- //The character needs to be a "-" //------------------------------------------------------- { if(currentChar != '-') { //Insert a dash m_sb.Insert(currentCharIndex, "-"); //If we added a character before the insert point, //advance the insert point if(currentCharIndex <= selectionStart) { selectionStart++; } } //This character is fine now, advance to the next char currentCharIndex++; } else //------------------------------------------------------- //The character needs to be a digit //------------------------------------------------------- { if(System.Char.IsDigit(currentChar) == false) { //Remove a character m_sb.Remove(currentCharIndex,1); //If we removed a character before the insert point, //retreat the insert point if(currentCharIndex < selectionStart) { selectionStart--; } //Don't advance the char count, we need to look at //the character that took the place of the one we //have removed } else { //The character is a digit, all is well. currentCharIndex++; } } } //If we are over the length, truncate it if(m_sb.Length > SSNumberLength) { m_sb.Length = SSNumberLength; } //Return our new string return m_sb.ToString(); } bool m_in_OnChangeFunction; protected override void OnTextChanged(EventArgs e) { //---------------------------------------------------- //If we change the .Text property, we will get called //re-entrantly. In this case we want to do nothing, and //just exit the function without passing on the event //to anyone else. //---------------------------------------------------- if(m_in_OnChangeFunction == true) { return; } //Note that we are now in the OnChanged function //so we can detect re-entrancy (see code above) m_in_OnChangeFunction = true; //Get the current .Text property string oldText = this.Text; //Get the current SelectionStart Index int selectionStart = this.SelectionStart; //Format the string so it meets our needs string newText = formatText_NNN_NN_NNNN(oldText, ref selectionStart); //If the text differs from the original, update the //.Text property if (System.String.Compare(oldText, newText ) != 0) { //This will cause us to get called re-entrantly this.Text = newText; //Update the location of the insert point this.SelectionStart = selectionStart; } //Because we have just forced the text entry into the //right format; if the length matches the length of //the Social Security number, we know that it is in //the format ###-##-####. if (this.Text.Length == SSNumberLength) { //Yes, we have a full Social Security number m_inputIsFullValidEntry = true; } else { //No, we do note have a full Social Security number yet m_inputIsFullValidEntry = false; } //Call our base class and let anyone who wants //to know that the text has changed get called base.OnTextChanged(e); //Note that we are exiting our code now and want to turn //off the re-entrancy check. m_in_OnChangeFunction = false; } protected override void OnKeyPress( System.Windows.Forms.KeyPressEventArgs e) { //Because we know we don't want any letters in our input, //just ingore them if we detect them. char keyPressed = e.KeyChar; if(System.Char.IsLetter(keyPressed)) { //Tell the system we have handled the event e.Handled = true; return; } //Process the key press as normal base.OnKeyPress (e); } //End function } //End class
Listing 13.4. Code in Form to Create the Custom TextBox Control
//----------------------------------------------------------- //The variable to hold our new TextBox control //----------------------------------------------------------- SocialSecurityTextBox m_filteredTextBox; //----------------------------------------------------------- //EVENT HANDLER: Create an instance of our custom control // and place it onto the form //----------------------------------------------------------- private void button1_Click(object sender, System.EventArgs e) { //Create, position and host the control m_filteredTextBox = new SocialSecurityTextBox(); m_filteredTextBox.Bounds = new System.Drawing.Rectangle(2,2,160, 20); //Hook up the event handler m_filteredTextBox.TextChanged += new EventHandler(this.textBox_TextChanged); //Set the parent m_filteredTextBox.Parent = this; //Select the control m_filteredTextBox.Focus(); //Disable this button so a second SocialSecurityTextBox does //not get created on top of this one button1.Enabled = false; } //--------------------------------------------------------- //EVENT HANDLER: This gets dynamically hooked up when the control // is created //--------------------------------------------------------- private void textBox_TextChanged(object sender, System.EventArgs e) { if (m_filteredTextBox.IsFullValidInput == true) { label1.Text = "FULL SOCIAL SECURITY NUMBER!!!"; } else { label1.Text = "Not full input yet..."; } }
Using Transparent Bitmap Regions Bitmaps with transparency masks are useful for many reasons. When writing a game, developers can use bitmaps with transparent regions to draw and move nonrectangular images around the screen play field. In mapping applications, bitmaps with transparency masks can be used to draw images on top of maps generated by other sources; drawing a location marker onto a map is a good example. Finally, the ability to integrate nonrectangular bitmaps onto other graphics is useful for creating good-looking user interfaces and business graphics.Bitmaps are essentially two-dimensional arrays of integers with each integer representing the color of a pixel at a specific location. For this reason, bitmaps are by their very nature rectangular. A nonrectangular image can be represented in a rectangular bitmap by declaring one color as the background color and filling the pixels outside the contained nonrectangular image with this color. Having a regular rectangular array of data has many advantages, not the least of which is the ability to easily copy a part of one image onto a part of another image; only very simple algorithms are required to do this if the regions are rectangular. The result of copying a rectangular portion of one bitmap onto another is simply one rectangle of image information replacing another; this is useful in many cases, but only allows the drawing of rectangular regions. This means that rectangular bitmaps containing nonrectangular images with a single background color are copied as opaque rectangles, foreground and background, to the destination. The result is not visually appealing. An alternative to doing this is to take a background image and hand-draw onto it all of the nonrectangular images, either using drawing functions (for example, .DrawLine(), .FillCircle()) or by setting the pixel data directly one pixel at a time. Both of these can produce good visual results, but are slow to perform and complex to write. What is needed is a way to copy one bitmap onto another but not copy over the background color. Figure 13.10 shows a simple game bitmap image; the bitmap is rectangular, but the image we want to draw is not. To be able to draw this image without its rectangular background, the function that copies over the bitmap needs to be told not to copy over pixels of our background color. In the .NET Compact Framework, this is done via the ImageAttributes class.Figure 13.10. A sample bitmap image with nonrectangular foreground and one-color background.
The ImageAttributes class has a SetColorKey() method that allows your code to set a masking color. This ImageAttributes class can then be passed as a parameter to one of the Graphics.DrawImage() function overloads; only one of the function overloads supports taking in this parameter. This DrawImage() function takes as parameters the source bitmap for the image we want to copy, the dimensions and location to copy to and from, and an ImageAttributes object that specifies what color mask will define transparency in the source image. All of the source image pixel information is then copied over onto the destination bitmap except for pixels matching the mask color specified in the ImageAttributes object. Source image pixels that are the same color as the mask are left uncopied. This allows the preexisting pixels in the destination image to "show through" the transparent regions in the bitmap we are copying. This copy operation is not quite as fast as a simple rectangular image copy, but it runs fairly quickly, making it an attractive option for working with nonrectangular images. When repeated or frequent drawing is required, it is a good idea to globally cache an ImageAttributes object that can be used in all your application's bitmap drawing code that deals with irregular shapes with transparent backgrounds; this saves the need to continually allocate and discard the ImageAttributes objects and reduces garbage buildup in your application's memory.Using Bitmaps with Transparent Regions Example Figure 13.11 shows an example of using transparent regions to meet advanced drawing needs. The first of the screenshots in Figure 13.11 shows the background image being drawn. In this case, the background image consists of black text drawn onto a white background. The second screenshot shows another image, a graphic consisting of a blue background with a yellow rectangle drawn on top of it along with two yellow ellipses. The third screenshot shows the second image drawn on top of the first image but with yellow defined as transparent. The result is that when the second image is copied onto the first, any pixels that are yellow in color are not copied over, leaving the original pixels in the destination bitmap showing through.Figure 13.11. Application showing drawing using transparent background. [View full size image] The code in Listing 13.5 enables you to build the application shown in Figure 13.11. To build the application, do the following:
1. | Start Visual Studio .NET (2003 or later) and create a C# Smart Device Application. | 2. | Choose Pocket PC as the target platform. (A project will be created for you and Pocket PC Form designer will appear.) | 3. | Add Button control to the form designer (it will be named button1), and rename it to buttonDrawBackground. | 4. | Double-click the button in the designer and fill in the code for buttonDrawBackground_Click() in the listing below. | 5. | Add a Button control to the Form designer and rename it to buttonDrawForeground. | 6. | Double-click the button in the designer and fill in the code for buttonDrawForeground_Click() in the listing below. | 7. | Add a Button control to the Form designer and rename it to buttonDrawBackgroundPlusForeground. | 8. | Double-click the button in the designer and fill in the code for buttonDrawBackgroundPlusForeground _Click() in the listing below. | 9. | Enter all of the remaining code in the listing below. | 10. | Go back to the form's designer. | 11. | Set the MinimizeBox property of the form to false. At runtime this will give the form an OK box at the top-right corner that makes it easy to close the form and exit the application. This is very useful for repeated testing. | 12. | Run the application; you should be able to reproduce the images in Figure 13.11. |
More Fun with Transparent Bitmaps The choice of yellow as the transparency color is completely arbitrary. It would be just as easy to choose blue as the transparency color and allow everything in the foreground bitmap that was yellow to be copied over and everything that was blue to be treated as transparent. It would also be simple to reverse the background and foreground bitmaps and make the yellow and blue rectangles and ellipses be the background and the black and white text the foreground; in this case, we would need to choose either black or white as our transparency color, and the text or its background would be made a transparent depending on our decision. Transparencies are a powerful concept and can be used to great effect with all kinds of bitmap images. |
Listing 13.5. Code in Form to Demonstrate the Use of Transparencies
//------------------------------------------------------- //Dimensions for our bitmaps and the onscreen PictureBox //------------------------------------------------------- const int bitmap_dx = 200; const int bitmap_dy = 100; //------------------------------------------------------- //Creates and draws the background image //------------------------------------------------------- System.Drawing.Bitmap m_backgroundBitmap; void CreateBackground() { if(m_backgroundBitmap == null) { m_backgroundBitmap = new Bitmap(bitmap_dx, bitmap_dy); } //Make the bitmap white System.Drawing.Graphics gfx; gfx = System.Drawing.Graphics.FromImage(m_backgroundBitmap); gfx.Clear(System.Drawing.Color.White); //Draw a bunch of text in black System.Drawing.Brush myBrush; myBrush = new System.Drawing.SolidBrush( System.Drawing.Color.Black); for(int y = 0; y < bitmap_dy; y = y + 15) { gfx.DrawString("I am the BACKGROUND IMAGE...hello", this.Font, myBrush, 0, y); } //Clean up myBrush.Dispose(); gfx.Dispose(); } //------------------------------------------------------- //Creates and draws the foreground image //------------------------------------------------------- System.Drawing.Bitmap m_foregroundBitmap; void CreateForeground() { if(m_foregroundBitmap == null) { m_foregroundBitmap = new Bitmap(bitmap_dx, bitmap_dy); } //Make the whole bitmap blue System.Drawing.Graphics gfx; gfx = System.Drawing.Graphics.FromImage(m_foregroundBitmap); gfx.Clear(System.Drawing.Color.Blue); //Draw some shapes in yellow System.Drawing.Brush yellowBrush; yellowBrush = new System.Drawing.SolidBrush( System.Drawing.Color.Yellow); gfx.FillEllipse(yellowBrush, 130, 4, 40, 70); gfx.FillRectangle(yellowBrush, 5, 20, 110, 30); gfx.FillEllipse(yellowBrush, 60, 75, 130, 20); //Clean up yellowBrush.Dispose(); gfx.Dispose(); } //------------------------------------------------------- //Sets the size and left hand location of the PictureBox //------------------------------------------------------- private void SetPictureBoxDimensions() { pictureBox1.Width = bitmap_dx; pictureBox1.Height = bitmap_dy; pictureBox1.Left = 20; } //-------------------------------------------------------------- //EVENT HANDLER: Display the BACKGROUND image in the PictureBox //-------------------------------------------------------------- private void buttonDrawBackground_Click(object sender, System.EventArgs e) { SetPictureBoxDimensions(); CreateBackground(); pictureBox1.Image = m_backgroundBitmap; } //-------------------------------------------------------------- //EVENT HANDLER: Display the FOREGROUND image in the PictureBox //-------------------------------------------------------------- private void buttonDrawForeground_Click(object sender, System.EventArgs e) { SetPictureBoxDimensions(); CreateForeground(); pictureBox1.Image = m_foregroundBitmap; } //-------------------------------------------------------------- //EVENT HANDLER: Overlay the FOREGROUND image ON TOP OF the // BACKGROUND image. Use a TRANSPARENCY MASK // so that the color YELLOW in the FOREGROUND // image becomes transparent and shows the // contents of the BACKGROUND IMAGE //-------------------------------------------------------------- private void buttonDrawBackgroundPlusForeground_Click( object sender, System.EventArgs e) { SetPictureBoxDimensions(); CreateForeground(); CreateBackground(); //Get the graphics of the BACKGROUND image because that //is what we are going to draw on top of. System.Drawing.Graphics gfx; gfx = System.Drawing.Graphics.FromImage(m_backgroundBitmap); //-------------------------------------------------------- //Create an ImageAttributes class. This class allows us //to set the TRANSPARANCY COLOR for our draw operation //-------------------------------------------------------- System.Drawing.Imaging.ImageAttributes trasparencyInfo = new System.Drawing.Imaging.ImageAttributes(); //-------------------------------------------------------- //Set the transparency color //-------------------------------------------------------- trasparencyInfo.SetColorKey(System.Drawing.Color.Yellow, System.Drawing.Color.Yellow); //Set our drawing rectangle System.Drawing.Rectangle rect = new System.Drawing.Rectangle(0,0, m_backgroundBitmap.Width, m_backgroundBitmap.Height); //-------------------------------------------------------- //Draw the FOREGROUND on top of the BACKGROUND bitmap and //use the Transparency Color in the ImageAttributes to //give us a transparent window onto the background //-------------------------------------------------------- gfx.DrawImage(m_foregroundBitmap, rect, 0,0, m_foregroundBitmap.Width, m_foregroundBitmap.Height, System.Drawing.GraphicsUnit.Pixel, trasparencyInfo); //Cleanup gfx.Dispose(); //Show the results in the bitmap pictureBox1.Image = m_backgroundBitmap; }
Embedding Images as Resources in Your Application Many applications make use of bitmap images to present a rich user interface to users. Custom buttons with images painted on them, graphics images used in drawing, logo images, or other background images all can improve the visual aesthetics of mobile device applications. Games often make extensive use of predrawn images rather than drawing complex images from scratch. For these reasons, it is often useful to have images packaged with your application's binary code. These images are then automatically deployed along with the application; this is much more robust than needing to manage the deployment of a set of image files along with your application. There is no size penalty for embedding images in your application versus deploying them as separate files; in both cases, they are binary image streams that your application accesses as needed.To use binary resources embedded in your application, two things are needed:The images must be compiled into your application. This can be specified at design time in the development environment.Your application must know how to locate the resources at runtime. Binary resources embedded in compiled .NET assemblies are addressed using syntax similar to file paths, specifying where the resource is located inside the loaded assemblies of your application at runtime.Both of these are described next.How to Embed an Image in Your Application It is relatively straightforward to use Visual Studio .NET to compile binary resources into your application.
1. | Start Visual Studio .NET. | 2. | Create a C# Smart Device Application project. | 3. | Choose the menu Project->Add Existing Item. | 4. | In the Add Existing Item dialog, change the Files of Type filter setting to show Image Files. | 5. | Locate the image file you want to add and choose it. (Note: I strongly suggest choosing an image file that is smaller than 300KB; otherwise you will be building a giant image file into your application that is probably significantly bigger than the application itself; large-resolution bitmaps may cause the device to run out of memory when loaded at runtime.) For the sake of this example, let's assume you chose a file called MyImage.PNG. | 6. | In the Visual Studio .NET Solution Explorer window, select the image file you have just added (MyImage.PNG), right-click it, and select the Properties menu item. | 7. | In the Properties window, look at the Build Action property. By default it is set to Content; this means that the file will be copied down to the device alongside the application when it is deployed. | 8. | Change the Build Action property to be Embedded Resource; this means that the binary contents of MyImage.PNG will be built in to the application's executable image every time the application is compiled. |
The Names of Embedded Resources are Case Sensitive Regardless of whether your programming language is case sensitive (C# is, VB.NET is not), embedded resource names are. Pay careful attention to the case of the letters in the filenames of the files you make embedded resources; your application will need to use the exact same casing to locate the resource streams at runtime. If the resource stream you are trying to find cannot be found because the name does not match exactly, an exception will be thrown by the runtime. This is a common mistake and can be very frustrating to track down if you do not know to look for case-sensitivity problems. |
How to Access an Embedded Image in Your Application The code in Listing 13.6 shows how to load a bitmap image from an embedded resource stream in your application. As above, we are using the image filename MyImage.PNG as our example image. The code should be placed inside a form with a Button and a PictureBox control. As with previous examples, the button1_click event should be wired to the button1 control by double-clicking the button in the designer to generate the function outline.Listing 13.6. Code in Form to Demonstrate the Loading of Embedded Resources
System.Drawing.Bitmap m_myBitmapImage; //------------------------------------------- //Loads an image that is stored as a binary //resource inside our assembly //------------------------------------------- public void LoadImageFromResource() { //If we have already loaded the bitmap //no point in doing it again. if(m_myBitmapImage != null) {return;} //---------------------------------------------------- //Get a reference to our application's binary assembly //---------------------------------------------------- System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); //------------------------------------------- //Get the name of the assembly //------------------------------------------- System.Reflection.AssemblyName thisAssemblyName = thisAssembly.GetName(); string assemblyName = thisAssemblyName.Name; //-------------------------------------------------------- //Stream the image in from our assembly and create an //in-memory bitmap // //NOTE: The ResourceStream name is CASE SENSITIVE, // be sure the image name matches EXACTLY with // file name of the image file you add to the project //-------------------------------------------------------- m_myBitmapImage = new System.Drawing.Bitmap( thisAssembly.GetManifestResourceStream( assemblyName + ".MyImage.PNG")); } //-------------------------------------------------------- //Load the image and display it in a picture box //-------------------------------------------------------- private void button1_Click(object sender, System.EventArgs e) { LoadImageFromResource(); pictureBox1.Image = m_myBitmapImage; }
Image Storage Formats and Bitmap Transparencies When using bitmaps in your application, it is important to think about the image formats you are using. For tiny bitmaps (for example, 8 x 8 pixels), the image compression does not matter much because the images are small anyway. For larger bitmaps, it may be possible to save a considerable amount of space by using lossy (for example, _.JPG) or nonlossy (for example, _.PNG) image-compression formats versus using noncompressed (for example, _.BMP) formats. An uncompressed screen-sized background image can add a lot of size to your application. Care must be taken in choosing storage formats for images that are going to have one color in the image treated as transparent at runtime; in these cases, only nonlossy compression should be used. Lossy compression can often yield higher-storage efficiency, but this is gained by giving up control of the color of each individual pixel in the image; you are getting an approximate image. A bitmap image that has transparent regions needs to be able to exactly specify the color of each pixel. |
Summary Building a great mobile device user interface can be a challenging and engrossing design problem. First, you must either adapt your design idea to the usage patterns and form factor of the selected target device or choose the appropriate mobile device type that meets your design goals. Devices differ significantly from desktop and laptop computers in their user interface capabilities and user-usage patterns. Different classes of devices also significantly differ from one another. Choosing a one-size-fits-all approach to developing a mobile device user interface intended to run on different classes of devices guarantees a poor fit for everyone. Choose your target devices and optimize your mobile user interface design for each of them to provide the best possible experience.It is important to come to an explicit design decision as to whether your application's user interface will be driven by one-handed or two-handed usage. Often this decision is dictated by the hardware you choose to target; for example, touch screens are two handed and phone keypads are usually one handed. Make an explicit written statement in your design document about the one- or two-handedness of the application and enforce this design decision throughout your design and development. When designing one-handed user interfaces consistent and simple "click through" is important; do not make the user switch from one button to another when running down common navigation paths in the application. The user should not have to shift his or her vision from device to keypad while navigating your application; this is distracting and breaks the flow of his thought.The size of the screen being targeted by your mobile device application will have a big impact on your design. Smaller screens tend to use list-oriented user interfaces; larger screens with touch pads or mouse pointers can often benefit from a tab-dialog metaphor for navigation between screens. Divide your mobile application's functionality into purpose-focused screens. Think carefully about the information you want to display on each screen and how navigation between screens is facilitated. Desktop applications tend to display several paragraphs of information simultaneously. Pocket PC-type applications tend to display one paragraph of information simultaneously with tab links to other paragraphs. Smartphones display list outlines that enable the user to drill into and view each chunk of information separately.Because mobile devices tend not to have full-sized keyboards, rapid entry of text is not a strength of most mobile devices. For this reason, when text data needs to be entered, it is useful to spend design time optimizing the user's productivity in entering common types of information. Often specialized user interfaces with larger buttons or customized user interfaces can aid the user in entering specific data such as dates, numbers, currency, addresses, or any other data that is not free-form text. External devices such as bar code scanners and speech- and visual-recognition systems can be a good way to speed up the input of specific data, but your mobile application should always give the user a manual way to key-in this information if the external input system fails, as inevitably it occasionally will.Software emulators for mobile devices are extremely useful in speeding up development, but they are no substitute for usability testing using real devices. An emulator image running on a laptop is not sufficient for testing a handheld mobile application's usability. Navigation, input, balance, comfort, and data visibility while working with the device are issues that can be accurately assessed only when running on physical hardware.When writing the code for your mobile device's user interface, it is important to structure your code in such a way that it will be easy to iterate on the user interface's design. Getting a mobile device user interface design correct will require several successive iterations on the layout and navigation of your application. Your mobile application's code will require flexibility to accommodate these changes. Centralizing the user interface management code in a state machine and using wrapper methods to insulate one control's event handling from another's updating are good ways to ensure flexibility in your design. It is fundamentally important to avoid distributing responsibility for managing your user interface; the more you can centralize the code that drives your user interface, the easier it will be to modify that code when the need arises to do so. Using helper classes to group together related user interface functions, such as all the event handlers for controls on a given display tab, also helps insulate user interface code from other code. Having a clean abstracted interface between your application logic and presentation layer logic is extremely important to maintain design flexibility. An added benefit to building well-encapsulated user interface code is that it will be much easier to port your mobile device application to other classes of devices.The .NET Compact Framework offers some advanced user interface functionality that can be useful when building rich mobile device user interfaces. The .NET Compact Framework enables developers to create two kinds of custom controls: (1) custom controls that implement their own visual display characteristics from scratch and derive from System.Windows.Forms.Control, and (2) custom controls that implement behaviors on top of existing controls by deriving from and extending nonabstract controls such as the System.Windows.Forms.TextBox control. Both types of custom controls are useful. The ability to dynamically create controls at runtime can be very useful; building a design-time version of a .NET Compact Framework control can be a laborious task, and unless you are in the market of selling custom controls you may want to forego this step and just create custom control instances dynamically at runtime.The ability to define transparency colors in bitmap copy operations allows your application to create rich graphical displays useful in entertainment, productivity, and scientific applications. The System.Drawing.Imaging.ImageAttributes class is the key to drawing with transparencies when using the .NET Compact Framework; it allows your code to set the color key that will be treated as transparent when copying a source bitmap onto a destination bitmap. Transparency regions can be useful when working with all kinds of bitmap images, from text and shapes dynamically drawn onto bitmaps to predrawn images loaded at runtime. Bitmap images used by your application can be stored as part of your application's binary image; doing this makes it easier to deploy your application by reducing the number of files your application is dependent upon.User interface design for mobile applications is both challenging and rewarding and can be a lot of fun. It requires a great deal of creativity to distil what the essential information is that needs to be presented to users and what the right application navigation metaphors should be. It is exciting to experiment, and you will learn rapidly as you try out different concepts. As emphasized in the earlier sections of this book, the perception of good performance by the user is essential to building a good mobile device user experience. If you keep performance top of mind, design your user interfaces with creativity, and write your code with disciplined flexibility built in, you will have a great experience working and experimenting with mobile user interface design. |