Part 1 – Designing the Framework
Introduction
I’ve always wanted to do this. Somehow, I never got round to it. With Christmas creeping closer, I decided that my Christmas present this year will be this project. From Hannes. To Hannes. With this article, we will learn how to add a window to an OpenFileDialog. This window will provide us with a preview of the selected picture, along with other picture specific details.
Design
Start Visual Studio and create a VB Windows Forms Project. Name it anything you like. Because there is so much code, I’m going to take a little shortcut for a change. I will not indicate each control’s name and values. I am attaching my working sample here, in case there is any confusion.
Design your default form to look like the following picture:
Figure 1 – frmOpenPics
Add a UserControl to your project. Name it HTG_PreviewPics and design it to resemble Figure 2.
Figure 2 – HTG_PreviewPics
Lastly, add yet another UserControl to your project with the name of HTG_Preview2. All you need to do here is to ensure that the usercontrol’s size is exactly the same as HTG_PreviewPics’ size. You do not need to add the groupboxes and labels here, only an OpenFileDialog with the same name (ofdOpen).
Coding
Right, time for action! Make sure you have enough beverages close by and enough snacks, because this code will take a while!
OS_APIs
Add a class to your project and name it OS_APIs. This class will host all the Windows APIs with their structures, enumerations and constants. If you have never encountered APIs before, have a look at this article. The link I supplied will not seem relevant, but actually is, especially if you’re new to APIs. I also recommend having a look at the API FAQ section here.
Enough talk, let’s get down to business.
Add the following Imports statements to your OS_APIs class :
Imports and Organization of code
Imports System.Runtime.InteropServices 'API Functions Imports System.Text
These namespaces help us to work with the API functions as well as use advanced Text features such as the StringBuilder.
Modify your class declaration as follows :
Namespace HTG_OpenPics_OSStuff Public NotInheritable Class OS_APIs 'Windows APIs + Windows Structs + Windows Constants + Enums Private Sub New() End Sub
Remember to add the ending statement for your newly created namespace at the bottom of your file. Why did I include the Namespace here? Well, it will make it easier to reference this class in all our other classes, as it gets loaded before the class referencing it does. It also helps with organizing your code a bit better. If you are not sure how namespaces work, have a look here.
API Declarations
Add the following code segment to your OS_APIs class :
#Region "APIs" <DllImport("user32.Dll")> _ Public Shared Function EnumChildWindows(ByVal hWndParent As IntPtr, ByVal lpEnumFunc As EnumWindowsCallBack, ByVal lParam As Integer) As Boolean End Function 'Enumerates the child windows that belong to the specified parent window by passing the handle to each child window, in turn, to an application-defined callback Public Delegate Function EnumWindowsCallBack(ByVal hWnd As IntPtr, ByVal lParam As Integer) As Boolean <DllImport("User32.Dll")> _ Public Shared Sub GetClassName(ByVal hWnd As IntPtr, ByVal param As StringBuilder, ByVal length As Integer) End Sub 'Retrieves the name of the class to which the specified window belongs <DllImport("user32.dll")> _ Public Shared Function GetClientRect(ByVal hwnd As IntPtr, ByRef rect As RECT) As Boolean End Function 'Retrieves the coordinates of a window's client area <DllImport("User32.Dll")> _ Public Shared Function GetDlgCtrlID(ByVal hWndCtl As IntPtr) As Integer End Function 'GetDlgCtrlID accepts child window handles as well as handles of controls in dialog boxes <DllImport("user32.dll", CharSet:=CharSet.Auto)> _ Public Shared Function GetParent(ByVal hWnd As IntPtr) As IntPtr End Function 'Retrieves a handle to the specified window's parent or owner <DllImport("user32.dll", SetLastError:=True)> _ Public Shared Function GetWindowInfo(ByVal hwnd As IntPtr, ByRef pwi As WINDOWINFO) As Boolean End Function 'Retrieves information about the specified window <DllImport("user32.dll")> _ Public Shared Function GetWindowRect(ByVal hwnd As IntPtr, ByRef rect As RECT) As Boolean End Function 'Retrieves the dimensions of the bounding rectangle of the specified window. <DllImport("user32.dll", CharSet:=CharSet.Auto)> _ Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As UInteger, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer End Function 'Sends the specified message to a window or windows <DllImport("user32.dll", CharSet:=CharSet.Auto)> _ Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer End Function <DllImport("user32.dll", CharSet:=CharSet.Auto)> _ Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, ByVal param As StringBuilder) As Integer End Function <DllImport("user32.dll", CharSet:=CharSet.Auto)> _ Public Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, ByVal chars As Char()) As Integer End Function <DllImport("user32.dll")> _ Public Shared Function SetParent(ByVal hWndChild As IntPtr, ByVal hWndNewParent As IntPtr) As IntPtr End Function 'Changes the parent window of the specified child window <DllImport("user32.dll", CharSet:=CharSet.Auto)> _ Public Shared Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As IntPtr, ByVal x As Integer, ByVal y As Integer, ByVal Width As Integer, ByVal Height As Integer, _ ByVal flags As SetWindowPosFlags) As Boolean End Function 'Changes the size, position, and Z order of a child, pop-up, or top-level window #End Region
Wow! What a mouthful!. OK, take a sip of your choice of beverage and look closely at the above code. This is what they call APIs. There are literally thousands of them, and we can make use of their code inside our program. Why do we need them? Simply because we will be doing system changes – that sounds more complicated than it is. We need these to resize the normal / standard / default open dialogbox and build our own stuff into it. That, is the system changes I’m talking about.
You will notice that I have commented on each API’s function. I found these descriptions on the best site (besides CodeGuru ) ever – MSDN. Later, as we go on, you will clearly see each API’s purpose. For now, let us continue adding the necessary Structures, Enums and Constants that work hand in hand with these functions.
Constants
Add the following System Constants :
#Region "Constants" Public Const CDM_FIRST = (&H400 + 100) 'FileNameChanged Constants Public Const CDM_GETFILEPATH = (CDM_FIRST + &H1) Public Const CDM_GETFOLDERPATH = (CDM_FIRST + &H2) Public Const CDN_FIRST = &HFFFFFDA7UI 'FolderChanged Constants Public Const CDN_SELCHANGE = (CDN_FIRST - &H1) Public Const CDN_FOLDERCHANGE = (CDN_FIRST - &H2) Public Const HWND_BOTTOM = 1 'ZOrder #End Region
These constants are used with the type of event that can happen inside the OpenFileDialog. They deal with two events: FileNameChanged and FolderChanged. We need to always update our custom dialog to reflect the changes happening.
Enums
Add the following Enumerations :
#Region "Enums" Public Enum FolderViewMode 'Shortened FolderViewMode Enum [Default] = &H7028 Thumbnails = [Default] + 5 End Enum Public Enum ImeNotify 'Input Method Manager Notify Shortened Enum IMN_CLOSESTATUSWINDOW = &H1 End Enum Public Enum Msg 'Shortened Msg Enum WM_ACTIVATE = &H6 WM_SHOWWINDOW = &H18 WM_WINDOWPOSCHANGING = &H46 WM_NOTIFY = &H4E WM_COMMAND = &H111 WM_IME_NOTIFY = &H282 End Enum Public Enum SetWindowPosFlags 'Shortened Set Windows Pos Flags Enum SWP_NOSIZE = &H1 SWP_NOMOVE = &H2 SWP_NOACTIVATE = &H10 SWP_HIDEWINDOW = &H80 SWP_NOOWNERZORDER = &H200 End Enum <Flags()> _ Public Enum SWP_Flags As UInteger SWP_NOSIZE = &H1 End Enum #End Region
These enums will be used with the APIs. They are just a bunch of “settings” available for certain API functions.
Structures
Lastly, add the Structures :
#Region "Structures" Public Structure NMHDR 'Contains information about a notification message Public hwndFrom As IntPtr Public idFrom As UInteger Public code As UInteger End Structure <StructLayout(LayoutKind.Sequential)> _ Public Structure OFNOTIFY 'Contains information about a WM_NOTIFY message sent to an OFNHookProc hook procedure for an Open or Save As dialog box 'Shortened Struct Public hdr As NMHDR Public lpOFN As IntPtr Public CDNShareViolation As IntPtr End Structure <StructLayout(LayoutKind.Sequential)> _ Public Structure RECT 'The RECT structure defines the coordinates of the upper-left and lower-right corners of a rectangle. Public left As UInteger Public top As UInteger Public right As UInteger Public bottom As UInteger Public Property Location() As Point Get Return New Point(CInt(left), CInt(top)) End Get Set(ByVal value As Point) right -= (left - CUInt(value.X)) bottom -= (bottom - CUInt(value.Y)) left = CUInt(value.X) top = CUInt(value.Y) End Set End Property Public Property Width() As UInteger Get Return right - left End Get Set(ByVal value As UInteger) right = left + value End Set End Property Public Property Height() As UInteger Get Return bottom - top End Get Set(ByVal value As UInteger) bottom = top + value End Set End Property End Structure <StructLayout(LayoutKind.Sequential)> _ Public Structure WINDOWINFO 'Contains window information Shortened Struct Public rcWindow As RECT Public rcClient As RECT Public cbSize As UInt32 Public dwStyle As UInt32 Public dwExStyle As UInt32 Public dwWindowStatus As UInt32 Public cxWindowBorders As UInt32 Public cyWindowBorders As UInt32 Public atomWindowType As UInt16 Public wCreatorVersion As UInt16 End Structure <StructLayout(LayoutKind.Sequential)> _ Public Structure WINDOWPOS 'Contains information about the size and position of a window. Public hwnd As IntPtr Public hwndAfter As IntPtr Public x As Integer Public y As Integer Public cx As Integer Public cy As Integer Public flags As UInteger End Structure #End Region
Time for a break. Eat a snack quickly, run to the little boys room quickly and reorganize your snacks and beverages.
OverrideCoreOFDDialog
Add a class named OverrideCoreOFDDialog to your project. This class will deal with the main OpenFileDialog window’s events. You may recall that I spoke about the FileNameChanged and FolderChanged event handlers earlier. Here, we will instruct the system’s OFD to run our own methods – which we will create later. Add the following code to this class:
Imports System.Runtime.InteropServices 'API Functions Imports System.ComponentModel 'UserControl Functions Imports System.Text 'Advanced String Functions Imports HTG_OpenPics.HTG_OpenPics_OSStuff.OS_APIs 'API Declarations / Structs / Enums Namespace HTG_OpenPics_UC Public Class OverrideCoreOFDDialog Inherits NativeWindow Implements IDisposable 'Event Handlers Public Delegate Sub FileNameChangedHandler(ByVal sender As OverrideCoreOFDDialog, ByVal strPicPath As String) Public Event FileNameChanged As FileNameChangedHandler Public Event FolderNameChanged As FileNameChangedHandler Private iptDialogHandle As IntPtr Public Sub New(ByVal iptHandle As IntPtr) iptDialogHandle = iptHandle AssignHandle(iptHandle) End Sub Public Sub Dispose() Implements System.IDisposable.Dispose ReleaseHandle() End Sub Protected Overrides Sub WndProc(ByRef m As Message) Select Case m.Msg Case CInt(Msg.WM_NOTIFY) 'Get FileNameChanged Event + FolderChange Events And Fire Our Own Instead Dim ofnNotify As OFNOTIFY = DirectCast(Marshal.PtrToStructure(m.LParam, GetType(OFNOTIFY)), OFNOTIFY) If ofnNotify.hdr.code = CUInt(CDN_SELCHANGE) Then Dim sbPicPath As New StringBuilder(256) SendMessage(GetParent(iptDialogHandle), CInt(CDM_GETFILEPATH), CInt(256), sbPicPath) RaiseEvent FileNameChanged(Me, sbPicPath.ToString()) ElseIf ofnNotify.hdr.code = CUInt(CDN_FOLDERCHANGE) Then Dim sbFolder As New StringBuilder(256) SendMessage(GetParent(iptDialogHandle), CInt(CDM_GETFOLDERPATH), CInt(256), sbFolder) RaiseEvent FolderNameChanged(Me, sbFolder.ToString()) End If Exit Select End Select MyBase.WndProc(m) End Sub End Class End Namespace
Here, we created temporary event handlers for our real events that will be fired via the use of the SendMessage API, inside the WndProc procedure. WndProc invokes the default window procedure associated with the OFD window.
BuildOpenDialog
Now, the fun part. Hungry? Thirsty?
This class is responsible in most part, for the displaying of our preview pane. We need to know where we want our Picture preview. We need to update our preview pane’s user interface when a file selection has been made. Follow? Good.
Imports and Class definition
Add and modify your code to look like the next code segment:
Imports System.Runtime.InteropServices 'API Functions Imports System.ComponentModel 'UserControl Functions Imports System.Text 'Advanced String Functions Imports HTG_OpenPics.HTG_OpenPics_OSStuff.OS_APIs 'API Declarations / Structs / Enums Namespace HTG_OpenPics_UC Public Class BuildOpenDialog 'Override OFD Functionalities Inherits NativeWindow 'Provides a low-level encapsulation of a window handle and a window procedure. Implements IDisposable 'Release unmanaged resources
Variables & Constructor
Add the following underneath your previous code:
Private onbBaseDialog As OverrideCoreOFDDialog 'Contains main OFD Events to Override Private htpSource As HTG_Preview2 'Preview Pane Private szOrig As Size 'Original OFD Size Private iptOpenHandle As IntPtr 'OFD Handle Private iptListView As IntPtr 'OFD ListView Window Handle Private wiListView As WINDOWINFO 'OFD ListView Window Information Private rctOpenWin As New RECT() 'Coordinates For OFD Window Private rctOpenClient As New RECT() Private blnClosing As Boolean = False 'Closing? Private blnLoaded As Boolean = False 'Loaded Into Memory? Public Sub New(ByVal iptHandle As IntPtr, ByVal ctlSource As HTG_Preview2) 'Constructor iptOpenHandle = iptHandle 'Determine Handles htpSource = ctlSource 'Source Control AssignHandle(iptOpenHandle) End Sub
Subs & Functions
Add the following events and procedures:
Private Sub OS_OverrideNativeOpenDialog_FileNameChanged(ByVal sender As OverrideCoreOFDDialog, ByVal strPicPath As String) If htpSource IsNot Nothing Then 'If Source Control Visible / Active htpSource.OnFileNameChanged(strPicPath) 'Call Event Appropriate Handler End If End Sub Private Sub OS_OverrideNativeOpenDialog_FolderNameChanged(ByVal sender As OverrideCoreOFDDialog, ByVal strPicFolder As String) If htpSource IsNot Nothing Then 'If Source Control Visible / Active htpSource.OnFolderNameChanged(strPicFolder) End If End Sub Public Property Closing() As Boolean 'Are We Closing the Dialog? Get Return blnClosing End Get Set(ByVal value As Boolean) blnClosing = value End Set End Property Public Sub Dispose() Implements System.IDisposable.Dispose 'Remove From Memory ReleaseHandle() 'Release All Window Handles References If onbBaseDialog IsNot Nothing Then 'Remove All Events RemoveHandler onbBaseDialog.FileNameChanged, New _ OverrideCoreOFDDialog.FileNameChangedHandler(AddressOf OS_OverrideNativeOpenDialog_FileNameChanged) RemoveHandler onbBaseDialog.FolderNameChanged, New _ OverrideCoreOFDDialog.FileNameChangedHandler(AddressOf OS_OverrideNativeOpenDialog_FolderNameChanged) onbBaseDialog.Dispose() End If End Sub Private Sub GetAllHandlers() 'Get OFD Window and Sub Window Handles EnumChildWindows(iptOpenHandle, New EnumWindowsCallBack(AddressOf ofdEnumWindowCallBack), 0) End Sub Private Function ofdEnumWindowCallBack(ByVal hwnd As IntPtr, ByVal lParam As Integer) As Boolean Dim sbClassName As New StringBuilder(256) 'Hold Class Names of Windows GetClassName(hwnd, sbClassName, sbClassName.Capacity) 'Get Sub Windows Class Names So We Can Reference It Dim intControlID As Integer = GetDlgCtrlID(hwnd) 'Control ID Dim wiWindow As WINDOWINFO 'Window Information Object GetWindowInfo(hwnd, wiWindow) 'Get Window Information 'OFD Dialog Window If sbClassName.ToString().StartsWith("#32770") Then onbBaseDialog = New OverrideCoreOFDDialog(hwnd) 'Override Events AddHandler onbBaseDialog.FileNameChanged, New _ OverrideCoreOFDDialog.FileNameChangedHandler(AddressOf OS_OverrideNativeOpenDialog_FileNameChanged) AddHandler onbBaseDialog.FolderNameChanged, New _ OverrideCoreOFDDialog.FileNameChangedHandler(AddressOf OS_OverrideNativeOpenDialog_FolderNameChanged) Return True End If Select Case DirectCast(intControlID, OFDControls) 'Determine Current Subwindows Case OFDControls.DefView 'ListView iptListView = hwnd GetWindowInfo(hwnd, wiListView) If htpSource.vmDefault <> FolderViewMode.[Default] Then SendMessage(iptListView, CInt(Msg.WM_COMMAND), CInt(htpSource.vmDefault), 0) End If Exit Select 'Can Use All Sub Windows Here If Needed End Select Return True End Function Private Sub Init() 'Initialize Everything blnLoaded = True 'Get Inner and Outer Window Rectangles - Dimensions GetClientRect(iptOpenHandle, rctOpenClient) GetWindowRect(iptOpenHandle, rctOpenWin) 'Obtain All Window Handles GetAllHandlers() 'Resize OFD to Compensate For Our Preview Pane Select Case htpSource.locStartup Case WinLoc.Right 'Right Side Of OFD 'Set Preview Pane Location htpSource.Location = New System.Drawing.Point(CInt(rctOpenClient.Width - htpSource.Width), 0) 'Make Preview Pane A Child Of The Normal OFD SetParent(htpSource.Handle, iptOpenHandle) 'Set Proper Z-Order For Control SetWindowPos(htpSource.Handle, HWND_BOTTOM, 0, 0, 0, 0, _ SetWindowPosFlags.SWP_NOACTIVATE _ Or SetWindowPosFlags.SWP_NOMOVE _ Or SetWindowPosFlags.SWP_NOSIZE) Exit Select 'We Could Use Bottom, Left Etc. Here As Well End Select End Sub
The above procedures sets up our preview pane for display and updating procedures. All we need to do now is to use the WndProc event here, to ensure these settings and events get called when they should. Let us add the WndProc procedure now:
Protected Overrides Sub WndProc(ByRef m As Message) 'Window Procedure Select Case m.Msg 'What Message Has Been Fired Case CInt(Msg.WM_SHOWWINDOW) 'OFD Is Showing blnLoaded = True 'Everything Is Loaded Init() Exit Select Case CInt(Msg.WM_WINDOWPOSCHANGING) 'Compensate For Moving Of OFD Window If Not blnClosing Then 'If Still In Memory If Not blnLoaded Then Dim pos As WINDOWPOS = DirectCast(Marshal.PtrToStructure(m.LParam, GetType(WINDOWPOS)), WINDOWPOS) 'Get Window Position Of OFD If htpSource.locStartup = WinLoc.Right Then 'Move Preview Pane Together With OFD If pos.flags <> 0 AndAlso ((pos.flags And CInt(SWP_Flags.SWP_NOSIZE)) <> CInt(SWP_Flags.SWP_NOSIZE)) Then szOrig = New Size(pos.cx, pos.cy) pos.cx += htpSource.Width Marshal.StructureToPtr(pos, m.LParam, True) End If End If 'Could Use Other Directions / Locations Here As Well End If Dim rctCurrSize As RECT Select Case htpSource.locStartup Case WinLoc.Right rctCurrSize = New RECT() GetClientRect(iptOpenHandle, rctCurrSize) htpSource.Height = CInt(rctCurrSize.Height) Exit Select 'Could Use Other Directions / Locations Here As Well End Select End If Exit Select Case CInt(Msg.WM_IME_NOTIFY) 'Get IME Messages If m.WParam = ImeNotify.IMN_CLOSESTATUSWINDOW Then 'Closing? Revert Back To Old OFD blnClosing = True htpSource.OnClosingDialog() SetWindowPos(iptOpenHandle, IntPtr.Zero, 0, 0, 0, 0, _ SetWindowPosFlags.SWP_NOACTIVATE _ Or SetWindowPosFlags.SWP_NOOWNERZORDER _ Or SetWindowPosFlags.SWP_NOMOVE _ Or SetWindowPosFlags.SWP_NOSIZE _ Or SetWindowPosFlags.SWP_HIDEWINDOW) GetWindowRect(iptOpenHandle, rctOpenWin) SetWindowPos(iptOpenHandle, IntPtr.Zero, CInt(rctOpenWin.left), CInt(rctOpenWin.top), CInt(szOrig.Width), CInt(szOrig.Height), _ SetWindowPosFlags.SWP_NOACTIVATE _ Or SetWindowPosFlags.SWP_NOOWNERZORDER _ Or SetWindowPosFlags.SWP_NOMOVE) End If Exit Select End Select MyBase.WndProc(m) End Sub
All this code and a whole bunch of errors when we attempt to run it. We obviously cannot run it now, because we have only now built the framework – we still need to build the hosting form, and ensure that everything displays and works correctly. That is what we do in Part 2. I have decided to draw the line here, because this information is quite a lot to soak up in one sitting. I am attaching the project’s source files here, just so you can make sure you have done everything correctly.
Conclusion
As said, we are only halfway done now. There is still a lot to learn and do. The onus is obviously on you now to do some further research on these APIs, to know exactly what makes them tick. With part 2, we will complete this project and make it work. I honestly didn’t think it will be so much work, but hey, it’s almost the holiday season – until then, we still need to work a great deal…. Sadly. Until next time, cheers!