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
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
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
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
<DllImport("user32.dll")> _
Public Shared Function GetClientRect(ByVal hwnd As IntPtr, ByRef rect As RECT) As Boolean
End Function
<DllImport("User32.Dll")> _
Public Shared Function GetDlgCtrlID(ByVal hWndCtl As IntPtr) As Integer
End Function
<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Public Shared Function GetParent(ByVal hWnd As IntPtr) As IntPtr
End Function
<DllImport("user32.dll", SetLastError:=True)> _
Public Shared Function GetWindowInfo(ByVal hwnd As IntPtr, ByRef pwi As WINDOWINFO) As Boolean
End Function
<DllImport("user32.dll")> _
Public Shared Function GetWindowRect(ByVal hwnd As IntPtr, ByRef rect As RECT) As Boolean
End Function
<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
<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
<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
#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)
Public Const CDM_GETFILEPATH = (CDM_FIRST + &H1)
Public Const CDM_GETFOLDERPATH = (CDM_FIRST + &H2)
Public Const CDN_FIRST = &HFFFFFDA7UI
Public Const CDN_SELCHANGE = (CDN_FIRST - &H1)
Public Const CDN_FOLDERCHANGE = (CDN_FIRST - &H2)
Public Const HWND_BOTTOM = 1
#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
[Default] = &H7028
Thumbnails = [Default] + 5
End Enum
Public Enum ImeNotify
IMN_CLOSESTATUSWINDOW = &H1
End Enum
Public Enum Msg
WM_ACTIVATE = &H6
WM_SHOWWINDOW = &H18
WM_WINDOWPOSCHANGING = &H46
WM_NOTIFY = &H4E
WM_COMMAND = &H111
WM_IME_NOTIFY = &H282
End Enum
Public Enum SetWindowPosFlags
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
Public hwndFrom As IntPtr
Public idFrom As UInteger
Public code As UInteger
End Structure
<StructLayout(LayoutKind.Sequential)> _
Public Structure OFNOTIFY
Public hdr As NMHDR
Public lpOFN As IntPtr
Public CDNShareViolation As IntPtr
End Structure
<StructLayout(LayoutKind.Sequential)> _
Public Structure RECT
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
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
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
Imports System.ComponentModel
Imports System.Text
Imports HTG_OpenPics.HTG_OpenPics_OSStuff.OS_APIs
Namespace HTG_OpenPics_UC
Public Class OverrideCoreOFDDialog
Inherits NativeWindow
Implements IDisposable
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)
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
Imports System.ComponentModel
Imports System.Text
Imports HTG_OpenPics.HTG_OpenPics_OSStuff.OS_APIs
Namespace HTG_OpenPics_UC
Public Class BuildOpenDialog
Inherits NativeWindow
Implements IDisposable
Variables & Constructor
Add the following underneath your previous code:
Private onbBaseDialog As OverrideCoreOFDDialog
Private htpSource As HTG_Preview2
Private szOrig As Size
Private iptOpenHandle As IntPtr
Private iptListView As IntPtr
Private wiListView As WINDOWINFO
Private rctOpenWin As New RECT()
Private rctOpenClient As New RECT()
Private blnClosing As Boolean = False
Private blnLoaded As Boolean = False
Public Sub New(ByVal iptHandle As IntPtr, ByVal ctlSource As HTG_Preview2)
iptOpenHandle = iptHandle
htpSource = ctlSource
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
htpSource.OnFileNameChanged(strPicPath)
End If
End Sub
Private Sub OS_OverrideNativeOpenDialog_FolderNameChanged(ByVal sender As OverrideCoreOFDDialog, ByVal strPicFolder As String)
If htpSource IsNot Nothing Then
htpSource.OnFolderNameChanged(strPicFolder)
End If
End Sub
Public Property Closing() As Boolean
Get
Return blnClosing
End Get
Set(ByVal value As Boolean)
blnClosing = value
End Set
End Property
Public Sub Dispose() Implements System.IDisposable.Dispose
ReleaseHandle()
If onbBaseDialog IsNot Nothing Then
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()
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)
GetClassName(hwnd, sbClassName, sbClassName.Capacity)
Dim intControlID As Integer = GetDlgCtrlID(hwnd)
Dim wiWindow As WINDOWINFO
GetWindowInfo(hwnd, wiWindow)
If sbClassName.ToString().StartsWith("#32770") Then
onbBaseDialog = New OverrideCoreOFDDialog(hwnd)
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)
Case OFDControls.DefView
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
End Select
Return True
End Function
Private Sub Init()
blnLoaded = True
GetClientRect(iptOpenHandle, rctOpenClient)
GetWindowRect(iptOpenHandle, rctOpenWin)
GetAllHandlers()
Select Case htpSource.locStartup
Case WinLoc.Right
htpSource.Location = New System.Drawing.Point(CInt(rctOpenClient.Width - htpSource.Width), 0)
SetParent(htpSource.Handle, iptOpenHandle)
SetWindowPos(htpSource.Handle, HWND_BOTTOM, 0, 0, 0, 0, _
SetWindowPosFlags.SWP_NOACTIVATE _
Or SetWindowPosFlags.SWP_NOMOVE _
Or SetWindowPosFlags.SWP_NOSIZE)
Exit Select
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)
Select Case m.Msg
Case CInt(Msg.WM_SHOWWINDOW)
blnLoaded = True
Init()
Exit Select
Case CInt(Msg.WM_WINDOWPOSCHANGING)
If Not blnClosing Then
If Not blnLoaded Then
Dim pos As WINDOWPOS = DirectCast(Marshal.PtrToStructure(m.LParam, GetType(WINDOWPOS)), WINDOWPOS)
If htpSource.locStartup = WinLoc.Right Then
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
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
End Select
End If
Exit Select
Case CInt(Msg.WM_IME_NOTIFY)
If m.WParam = ImeNotify.IMN_CLOSESTATUSWINDOW Then
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!