Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Windows Forms and Win32


Richard is the author of Programming with Managed Extensions for Microsoft Visual C++ .NET 2003 (Microsoft Press, 2003). He can be contacted at richard@ richardgrimes.com.


Windows Forms technology is built over Win32 windowing. To effectively use Windows Forms, you have to understand the principles of Win32 windowing—including the relationship of windows classes, message queues, and threads. In this article, I touch upon some Windows Forms topics and explain why you need a good understanding of Win32 windowing to write the best Windows Forms code.

Creating Windows and Forms

When you create a new Form, you haven't created a window—all you've done is create a .NET object. The object's constructor just creates the .NET object's side of things; that is, fields and event handlers are initialized. The actual Win32 window is only created the first time that the form is made visible. The Visible property calls the protected method SetVisibleCore. This method attempts to read the form's Handle, and the property get handler calls the protected method CreateHandle, which creates the window.

This two-stage construction means that the constructor should be used to initialize items that are not dependent upon the window's handle. In light of this, some controls cache data that requires a Windows handle to be used when the control handle has been created. For example, the ListView control is based on the list view common control. If you want to add an item to the control, you do so through the Items property, which is a ListViewItemCollection. The Add method of this class actually delegates the task to the ListView.InsertItem method, which tests to see if the list view control has been created. If so, it inserts a new LVITEM by sending the control the LVM_INSERTITEM message. If the control has not been created, then the ListViewItem objects are stored in an ArrayList. When the control is created, the HandleCreated event is raised, the handler for this code iterates through all the items in the cached ArrayList, adding those items to the control.

To create your own control, factor your code so that the operations that require the control to have a Windows handle can only be called after the handle is created. Or if the operations could be called before, cache the operations and replay them on the HandleCreated event handler.

Indented Items in a ListView

For example, one feature I want to use with the ListView class (available electronically; see "Resource Center," page 5) is the ability to indent the main item. If you use Outlook Express, you know what I mean. When you click on Local Folders, you see a list view that shows all the folders that you have, and nested folders are indented from their parent folders. To do this with Win32 is simple. Your list view must be in report view (LVS_REPORT, which Windows Forms calls View.Details) and it must have at least one column. Your items must also have icons because the indentation is in units of the width of an item's image. Then all you need to do is provide a value for the LVITEM.iIndent field that is passed to the control using the LVM_INSERTITEM message.

There is no mechanism to do this with the Windows Forms ListView class, so you have to derive from this class and create your own implementation. Listing One is a partial listing of a class to do this. The InsertIndented method inserts a ListViewItem with the specified indentation. InsertItem is the method that does the actual work by sending the LVM_INSERTITEM message. Because an item may have subitems, the SetItemText is a helper method to set the text of each subitem. Finally, if the list view has not been created, the information is put into an LVIData object and stored in the operations member. When the list view is created, the OnHandleCreated method is called, which iterates through the items in the operations field and inserts them into the list view.

You must first create the code to insert an item, and so you need to write the managed version of LVITEM and gain access to SendMessage using Platform Invoke; see Listing Two. There are just two points to make about this:

  • LVM_INSERTITEM is passed a pointer to an unmanaged LVITEM structure, but you do not have to worry about this because Platform Invoke does the work for you when you declare the final parameter as ref LVITEM. The ref indicates that a pointer is passed and LVITEM refers to the managed class.
  • This class is marked with [StructLayout(LayoutKind.Sequential)], which indicates that the fields are placed in memory in the order you specify. These items have the same size as the fields in the unmanaged LVITEM structure.

Listing Three accesses the list view control. InsertItem simply initializes an LVITEM object and calls SendMessage. The ListView class uses the lParam member to hold a unique ID to identify the item. To generate this ID, I need to use the same mechanism as the ListView class uses. It does this with a private method called GenerateUniqueID. Because this is private, I use reflection to access it from the base class. When the item is added to the control, I also add it to the private Hashtable in the base class called listItemsTable and update a field that holds the number of items. I don't like using reflection like this, but there is no other way to get access to private members.

The Items collection of the ListView class is based on a Hashtable of ListViewItem objects, each containing information about the item in the control including its display ID and the unique ID assigned to the item. These values are stored in private fields in ListViewItem and are assigned using the private method Host. The UpdateItem method uses reflection to call this method.

Listing Four is the remaining code. InsertIndented is the public method used to insert items into the list view control. First, it checks to see if the control has been created. If not, then the information is stored in the operations container; otherwise, InsertItem is called. The implementation of OnHandleCreated merely checks to see if there are any cached items, and if so, these are inserted using InsertItem. There is one caveat to this method. If you intersperse calls to InsertIndented with calls to ListView.Items.Add, then if the operations are cached, the final order of the items in the control will not be preserved. It is prudent to only use InsertIndented. Listing Five shows code that uses this control; the LoadIconFromResources method is not shown—you should implement it to load an icon from a file or obtain one from the application's resources.

A fair amount of code had to be written to let you add a simple functionality. Most of this code was required to let you bypass the existing mechanism to insert items into the control, while still allowing the existing functions to continue to work. It's a pity that Microsoft did not add this functionality to the ListView class.

Application Contexts

Listing Five shows the standard way to create a form: Pass a new instance of the form object to the Application.Run method. I mentioned earlier that the form's window is not created until the Visible property is set to True, so where does this happen? This is one of the responsibilities of the Run method, although this method does a lot more. Every Windows application needs a "message pump." In its simplest form, this calls the Win32 GetMessage function to retrieve the next message in the thread's message queue, then calls DispatchMessage to call the window procedure of the window that the message is intended for. Notice that I said the thread's message queue. Message queues, and hence windows, have thread affinity. If a window is created on a specific thread, then its messages will be sent to the message queue for that thread.

The application's main thread (or indeed, any thread with Thread.IsBackground set to false) will keep the application alive. If the main thread dies, so will the application's process. Similarly, if the thread keeps alive then, so will the process. However, windows have a different lifetime mechanism. Windows that have a caption bar have an adornment (the "X" button), and if users click this they expect the window to close. If the application has just one window, users expect closing this window to kill the application's process. Win32 developers handle this by implementing the message pump as a loop that breaks if GetMessage returns false. This happens if GetMessage reads the WM_QUIT message from the message queue. Consequently, you control the application's lifetime by posting this message to the main thread's message queue.

Application.Run is called on the main thread, and it implements the message pump. This means that the main thread is kept alive, handling the pump and dispatching messages to the various windows in the application. When this method stops reading messages and returns, the main thread dies and the application's process dies. However, this does bring up the question of how the message pump loop is broken. The answer lies in application contexts.

The Application class lets you register a main form for the thread where Application.Run is called. If you look at the documentation for this class, you'll see that all the members are static, which raises the question of where this information is held. Application has a nested class called ThreadContext and instances of this class are held in thread local storage; that is, each thread has a different instance. When you call Application.Run, the current thread's ThreadContext object is obtained.

The Application class has the method ExitThread, which a form could use to close down the message pump. Listing Six is a simple forms application that creates a form, makes it visible, and calls Application.Run with no parameters. The call to Run provides the message pump, which is stopped with a call to ExitThread in the Closed event handler. If you comment out this line and run this application, you see that clicking on the X adornment box closes the window. However, if you run Task Manager, you see that the process continues to run; without a window, you'll have to use Task Manager to close this process.

The version of Run that you usually call is the overload that takes a Form object. However, all this does is wrap the Form object in an ApplicationContext object and pass it to another overload of the Run method, which starts up the message pump. The ApplicationContext class is used as a bridge between a form and the implementation of the message pump, so that if the message pump ends, the main form closes, and if the main form closes, the message loop dies.

The message pump is on the ThreadContext object for the current thread (a method called RunMessageLoopInner). In addition, the ApplicationContext and its MainForm are cached as fields in the ThreadContext. When the message pump loop finishes, Dispose is called on the ThreadContext object, which enumerates all the windows created on the current thread and then disposes each one. This means that when the message pump ends, the forms are allowed to clean up their resources.

The other requirement is that if the form closes, then the message loop should be stopped. The ApplicationContext object has a method called OnMainFormDestroy, which is added to the form's HandleDestroyed event when the context object's MainForm property is set in the constructor. This event is the last one raised when a form window is destroyed; at this point, the form object is alive. OnMainFormDestroy calls ExitThread, which stops the message pump. Again, when the message pump loop finishes, all the forms created on the thread are disposed, and their Dispose methods are called to clean up the components they hold.

The first thing that ExitThread does is obtain the thread's ThreadContext object, then it tests to see if there is a context object. If so, Dispose is called on this object. ApplicationContext.Dispose merely releases the reference for the main form (if it has a reference) so that there is one less reference to prevent the form from being finalized. If there is no application context object, then the thread context object is disposed and if there are no messages in the message queue, then all the windows created on the thread are disposed as mentioned earlier. If there are messages in the message queue, the message pump should handle those messages, but no more, so ThreadContext.Dispose provides an asynchronous shutdown mechanism. It works like this: Dispose posts the custom message MSG_APPQUIT (an operation that does not block) to the thread's message queue. The message pump is still active at this point and so after it has handled all the other messages in the queue, the MSG_APPQUIT message is handled by disposing all the thread's windows, then posting the WM_QUIT message, which will finally end the loop.

As you can see, the mechanism to close down the message loop and handle windows disposal is ordered, and much of the code is similar to the code that you'll see in a Win32 application.

Threading

The final issue I want to mention is threading. The messages for a window are placed in the message queue for the thread that created the window, so if you interact with a control, you must do this on the GUI thread. On the other hand, the GUI thread (usually the main thread) spends all of its time pumping the message queue. When a message is retrieved, it is dispatched to the windows procedure for the appropriate window. The actual windows procedure is registered to be a method called WndProc in a class called ControlNativeWindow, a nested class in Control. This method does little processing and passes the message onto the WndProc method defined for the .NET control. This method, and the method it overrides in the control's base class, is effectively a huge switch statement that handles individual messages by raising events, just like any Win32 process.

To generate events, the control usually has a method with the prefix On (for example, OnResize), which obtains the delegate for the event (for example, Resize) and invokes it. You can handle the message either by overriding the On method or by adding a delegate to the event. If you override the On method, you must make sure that you call the base class implementation so that the event delegate is still invoked. The important point is that the thread that pumps the message queue is the same thread that runs the WndProc, which is the same thread that invokes the event delegate. So if your event handlers are lengthy, then you are preventing the pumping of the message queue, which means that messages intended to update the UI are not handled in a timely fashion. It makes sense to execute lengthy operations on another thread.

However, when an operation interacts with the UI, it does so by generating messages, and as I have already mentioned those messages must be sent and handled on the correct thread. Windows Forms provides a mechanism to do this. The Control class implements an interface called ISynchronizedInvoke with three methods and a property. The InvokeRequired property indicates whether the control is thread safe. If you try to access it from another thread, you should use Invoke. Invoke is passed a delegate and an array of arguments; the delegate is the code that you want to be invoked on the GUI thread. In effect, this method checks to see if the current thread is the GUI thread. If it is, no marshaling is required and the delegate is invoked straight away. If the thread is not the GUI thread, then the delegate and parameters are put into a separate "job" object and added to a queue maintained by the control. The GUI thread is then informed by posting a custom message to its message queue. The handler for this message reads all the job objects in the control's queue and invokes the delegate on each one.

The other two methods on the interface are BeginInvoke and EndInvoke, which let you invoke the delegate synchronously. Of course, the whole point about Invoke is that the delegate is invoked on another thread, and the difference between this and the asynchronous methods is that Invoke blocks until the delegate has completed, whereas BeginInvoke returns as soon as the message is posted to the GUI thread and provides a call object that you can test for completion of the invocation. This only makes a difference if the delegate returns a value, in which case you should call EndInvoke to retrieve the data at a later time.

Conclusion

To effectively use Windows Forms, you need to have an understanding of how Win32 windowing works. Merely knowing the mechanics of Windows Forms is not enough. To be able to use the library effectively and to prevent you from writing code that could have serious effects on the responsiveness of your user interface, you have to know and apply Win32 windowing principles.

DDJ



Listing One

class IndentListView : ListView
{
   public int InsertIndented(ListViewItem lvi, int indentLevel);
   protected int InsertItem(ListViewItem lvi, int indentLevel, int id);
   protected void SetItemText(int itemIndex, int subItemIndex, string text);
   class LVIData{};
   private ArrayList operations = null;
   protected override void OnHandleCreated(EventArgs e);
}
Back to article


Listing Two
[DllImport("user32")]
static extern int SendMessage(IntPtr hWnd, int msg, 
   int wParam, ref LVITEM lParam); 
[StructLayout(LayoutKind.Sequential)]
struct LVITEM 
{
   public const int LVIF_TEXT   = 0x0001;
   public const int LVIF_IMAGE  = 0x0002;
   public const int LVIF_PARAM  = 0x0004;
   public const int LVIF_INDENT = 0x0010;
   public const int LVM_INSERTITEM = 0x1007;
   public const int LVM_SETITEMTEXT = 0x102d; 

   public uint mask; 
   public int iItem; 
   public int iSubItem; 
   public uint state; 
   public uint stateMask; 
   public string pszText; 
   public int cchTextMax; 
   public int iImage; 
   public IntPtr lParam;
   public int iIndent;
   public int iGroupId;
   public uint cColumns;
   public uint puColumns;
}
Back to article


Listing Three
protected int InsertItem(ListViewItem lvi, int indentLevel, int id)
{
   int dispIdx = GetCount() + 1;
   LVITEM lvitem = new LVITEM();
   lvitem.mask = LVITEM.LVIF_TEXT | LVITEM.LVIF_PARAM 
      | LVITEM.LVIF_IMAGE | LVITEM.LVIF_INDENT;
   lvitem.iItem = dispIdx;
   lvitem.pszText = lvi.Text;
   lvitem.iImage = lvi.ImageIndex;
   lvitem.iIndent = indentLevel;
   lvitem.lParam = (IntPtr)id;

   AddToItemsTable(lvi, id);
   dispIdx = SendMessage(this.Handle, LVITEM.LVM_INSERTITEM, 0, ref lvitem);
   UpdateItem(lvi, id, dispIdx);

   for (int idx = 0; (idx < lvi.SubItems.Count); ++idx)
   {
      SetItemText(id, idx, lvi.SubItems[idx].Text);
   }
   return lvi.Index;
}
protected void SetItemText(int itemIndex, int subItemIndex, string text)
{
   LVITEM lvitem = new LVITEM();
   lvitem.mask = LVITEM.LVIF_TEXT;
   lvitem.iItem = itemIndex;
   lvitem.iSubItem = subItemIndex;
   lvitem.pszText = text;
   SendMessage(this.Handle, LVITEM.LVM_SETITEMTEXT, itemIndex, ref lvitem);
}
private int GenerateNextID()
{
   Type type = typeof(ListView);
   MethodInfo mi = type.GetMethod("GenerateUniqueID", 
      BindingFlags.NonPublic | BindingFlags.Instance);
   return (int)mi.Invoke(this, null);
}
private void AddToItemsTable(ListViewItem lvi, int id)
{
   Type type = typeof(ListView);
   FieldInfo fi = type.GetField("listItemsTable", 
      BindingFlags.NonPublic | BindingFlags.Instance);
   Hashtable listItemsTable = (Hashtable)fi.GetValue(this);
   listItemsTable.Add(id, lvi);
   fi = type.GetField("itemCount", 
      BindingFlags.NonPublic | BindingFlags.Instance);
   int count = (int)fi.GetValue(this);
   fi.SetValue(this, ++count);
}
private void UpdateItem(ListViewItem lvi, int id, int dispIdx)
{
   Type type = typeof(ListViewItem);
   MethodInfo mi = type.GetMethod("Host", 
      BindingFlags.NonPublic | BindingFlags.Instance);
   object[] args = new object[]{this, id, dispIdx};
   mi.Invoke(lvi, args);
}
Back to article


Listing Four
class LVIData
{
   public ListViewItem lvi;
   public int IndexLevel;
   public int ID;
}
public int InsertIndented(ListViewItem lvi, int indentLevel)
{
   if (this.Handle == IntPtr.Zero)
   {
      if (operations == null) 
         operations = new ArrayList();
      LVIData data = new LVIData();
      data.lvi = lvi;
      data.IndexLevel = indentLevel;
      data.ID = GenerateNextID();
      operations.Add(data);
      return data.ID;
   }
   return InsertItem(lvi, indentLevel, GenerateNextID());
}
protected override void OnHandleCreated(EventArgs e) 
{ 
   if (operations != null)
   {
      for (int idx = 0; idx < operations.Count; ++idx)
      {
         LVIData data = operations[idx] as LVIData;
         InsertItem(data.lvi, data.IndexLevel, data.ID);
      }
      operations = null;
   }
   base.OnHandleCreated(e); 
}
Back to article


Listing Five
public class MainForm : Form
{
   private IndentListView lv;
   public MainForm()
   {
      this.lv = new IndentListView();
      this.lv.Dock = DockStyle.Fill;
      this.lv.View = View.Details;
      ImageList il = new ImageList();
      Icon ic = LoadIconFromResources("first_icon");
      il.Images.Add(ic);
      lv.SmallImageList = il;

      ColumnHeader header;
      header = new ColumnHeader();
      this.lv.Columns.Add(header);
      header.Text = "Data";
      header.Width = 200;
      this.Controls.Add(this.lv);

      ListViewItem lvi = new ListViewItem("one", 0);
      lv.InsertIndented(lvi, 0);
      lvi = new ListViewItem("two", 0);
      lv.InsertIndented(lvi, 1);
      lvi = new ListViewItem("three", 0);
      lv.InsertIndented(lvi, 0);
   }
   static void Main()
   {
      Application.Run(new MainForm());
   }
}
Back to article


Listing Six
class MainForm : Form
{
   MainForm()
   {
      this.Closed += new EventHandler(ClosedForm);
   }
   void ClosedForm(object sender, EventArgs e)
   {
      Application.ExitThread();
   }
   static void Main()
   {
      MainForm form = new MainForm();
      form.Visible = true;
      Application.Run();
   }
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.