Introduction
A few days ago, I wanted to start creating an Icon Editor application to make use of my IconLib library.
I created my main form, and I thought �where do I need to start�. Then, I decided to create a menu with the Open functionality. I thought the Open feature should have a preview screen to see the icon before opening it.
If you are reading this page, probably it is because you know that .NET has an OpenFileDialog
class, but it cannot be customized. The objective of this control is to allow you to add some functionality to the OpenFileDialog
.NET class. The main reason why you can�t customize the OpenFileDialog
in .NET is because the class is declared sealed
which means you can�t inherit from it. If you go to the base class FileDialog
, it will allow you to inherit from it, but is has an internal abstract
method �RunFileDialog
�. Because it is internal
and abstract
, it only allows inheriting from it inside the same assembly.
How many times have you wanted to put some extra control in the OpenFileDialog
control and you couldn�t�
Searching for code for .NET, I found a couple places where they used MFC, but nothing for .NET. OpenFileDialog is not a native implementation in .NET, instead it makes use of a Win32 API �GetOpenFileName
�.
At this point, I had three choices:
- Create my own OpenFileDialog from scratch.
- Create my own OpenFileDialog reusing resources, (using the API �GetOpenFileName� and providing my own template).
- Hack the .NET
OpenFileDialog
and add the functionality I need for it.
Option (a) was not an option for me because it could require a lot of development time when I have a lot more stuff to be done. Later when the product is finished, I could review it. The next option required me to provide my own template using calls to Win32 API and resources. The option (c) was the more viable option at this time; don�t think of this as a bad hack, basically a hack is when you want to make the control do some extra functionality and you must do it from a different thread or process.
So because I like challenges, I decided to �hack� the OpenFileDialog
class to create my own customizable control.
What it can do for you
I could have hacked the control to do what I needed and that would be it, but I ran into this problem many times right from .NET 1.0, and no one so far had come with a solution for it, so I decided to create an interface to this control where it can be used in different applications.
Also, I wanted to create something that didn�t require changing or adding code to the current project, and be capable of adding multiple controls without knowing the details of how they work; it needed to be a standalone control that can be added just as any other control in the IDE.
I created this control and I called it �OpenFileDialogEx
�.
How do I do it?
Imagine OpenFileDialogEx
as an abstract class: the only reason I didn�t make this class abstract is because the VS IDE can�t create an instance of an abstract class which avoids the rendering in the screen.
You could use the OpenFileDialogEx
class like it is but make no sense, because it contains no extra functionality, just an empty UserControl
.
So you must inherit OpenFileDialogEx to create your own customized version of the Open File Dialog.
After you inherit OpenFileDialogEx
, you have created a custom control where you can add any control, you could add extra buttons, panels, or group boxes. Basically, it is a controls container; later this container will be �appended� to the .NET OpenFileDialog
object on the fly.
There are three extra properties, three methods, and two events in this control that are different from any UserControl
.
DefaultViewMode:
This property lets you choose which view the OpenFileDialog
should start in; by default, it opens using the �Details view�. Here you can specify a different default view like Icons, List, Thumbnail, Detail, etc.
StartLocation:
This property tells if the control created should be stacked on the right, bottom, or behind the classic OpenFileDialog
. Usually, this property will be the one used to expand the OpenFileDialog
horizontally. If instead, you need to add extra controls to the current OpenFileDialog
, then you can specify �None
� and the controls inside OpenFileDialogEx
will share the same client area with the original OpenFileDialog
.
OpenDialog:
This property is the embedded OpenFileDialog
inside the control. Here you can setup the standards property as InitialDir, AddExtension, Filters, etc.
OpenFileDialog
for default is resizable, OpenFileDialogEx
will help you with that automatically; the user control �OpenFileDialogEx
� will be resized automatically. When the user expands or shrinks the window, it will behave differently depending on the StartLocation
property.
StartLocation
Right
: user control will be resized vertically.
Bottom
: user control will be resized horizontally.
None
: user control will be resized horizontally and vertically.
Basically, when you add your controls as buttons, panels, group boxes etc., you have to set the Anchor
property of every control, then you can control where your control will be when the user resizes the OpenFileDialog
window.
For example, to have an image preview, you could set the start location at the right, add a PictureBox
to your inherited OpenFileDialogEx
and set the Anchor
property for the PictureBox
to be Left, Top, Right, Bottom
; this will resize the picture box dynamically when the user resizes the OpenFileDialog
.
The methods are virtual methods that you will override to interact with the original OpenFileDialog
.
OnFileNameChanged()
This method is called every time the user clicks on any file inside the view.
OnFolderNameChanged()
This method is called every time the user changes a folder from any control inside the OpenFileDialog
.
OnClosing()
This method is called when the OpenFileDialog
is closing, this is useful to release any resources allocated.
The two events are FileNameChanged
and FolderNameChanged
, those events are fired from their respective virtual methods �OnFileNameChanged
� and �OnFolderNameChanged
�. Instead of using the events, I recommend overriding the methods because it is cleaner code and also it doesn�t have another level of indirection.
How is it done?
The first problem is that OpenFileDialog
is a modal dialog. This means that basically you can�t get the handle of the window because when you call ShowDialog()
, you don�t have the control of the program flow as long the OpenFileDialog
is open.
One way to get the handle of the OpenFileDialog
is to override the WndProc
method on your form and watch for the messages. When OpenFileDialog
is created, the owner form will receive some messages like WM_IDLE
, WM_ACTIVATE
, WM_NC_ACTIVATE
, etc. Those entire set of messages will set the parameter lParam
with the handle to the OpenFileDialog
window.
As you see, this requires overriding the WndProc
methods. Some developers even don�t know that WndProc
exists, so I wanted to avoid that. Also. I noticed some problems with MDI windows opening an OpenFileDialog
.
Then what I did, basically, was when ShowDialog()
is called, it creates a dummy form off the screen and hides it, this form will take care of opening the OpenFileDialog
and taking the OpenFileDialog
window handle.
At first, it listened on the WM_IDLE
message, but the problem is when the message is send, it is already too late and the window is created and shown in the screen. Still, you can change the contents, but the user will see a small flicker on the screen between the original OpenFileDialog
and the customized version.
Instead, we could take the message WM_ACTIVATE
that happens before the OpenDialog
is show on the screen.
So far it gets the handle and it is ready to be shown, now what?
How will it change the properties for the OpenFileDialog
window?
Here is when the handy .NET NativeWindow
comes into the picture, a NativeWindow
is a window wrapper where it processes the messages sent by the handle associated to it. It creates a NativeWindow
and associates the OpenFileWindow
handle to it. From this point, every message sent to OpenFileWindow
will be redirected to our own WndProc
method in the NativeWindow
instead, and we can cancel, modify, or let them pass through.
In our WndProc
, we process the message WM_WINDOWPOSCHANGING
. If the open dialog is opening, then we will change the original horizontal or vertical size depending of the StartLocation
set by the user. It will increment the size of the window to be created. This happens only once when the control is opened.
Also, we will process the message WM_SHOWWINDOW
. Here, all controls inside the original OpenFileDialog
are created, and we are going to �append� our control to the open file dialog. This is done by calling a Win32 API �SetParent
�. This API lets you change the parent window. Then, basically what it does is �attach� our control to the original OpenFileDialog
in the location it set, depending on the value of the StartLocation
property.
The advantage of it is that we still have complete control over the controls attached to the OpenFileDialog
window. This means we can receive events, call methods, and do whatever we want with those controls.
Also, in the initialization, we will get the window handles for every control inside the original OpenFileDialog
. This allows again to create .NET NativeWindow
s to process the messages in every control.
Now everything is ready, how do we watch for the messages when the user clicks on the ListView
?
At first, I tried to process the messages from the ListView
itself, creating a NativeWindow
to it, but the problem is that every time the user changes the folder or clicks on a different view, the handler is destroyed and we have to recreate the handler to it.
Analyzing all windows inside FileOpenDialog
with MS Spy, we can notice another FileDialog
window inside the FileOpenDialog
, and very probably it is the base window of FileOpenDialog
. Checking the MSDN documentation, we see that every action made on the FileOpenDialog
fires a WM_NOTIFY
message filling a OFNOTIFY
struct; this struct contains a code of the action made, and two of those actions are CDN_SELCHANGE
and CDN_FOLDERCHANGE
.
They are called when the user interacts with the folder combo box or the list view. Then, first I get the handle to the base FileWindow
and I create a NativeWindow
from this handle. This allows to process the messages WM_NOTIFY
to analyze the OFNOTIFY
struct and process CDN_SELCHANGE
and CDN_FOLDERCHANGE
. When this window processes those messages, they are forwarded to the OpenFileDialogEx
control to the methods OnFileNameChanged
and OnFolderNameChanged
.
Another method was to intercept when the FileOpenDialog
window is closed. At first, I used the message WM_CLOSE
and it worked, but later I discovered that this message is not been called when the user double clicks on a file inside the list view. Watching the messages produced by FileOpenDialog
, I saw that I could use the WM_IME_NOTIFY
message. This message is sent with a wParam
value of IMN_CLOSESTATUSWINDOW
when the FileOpenDialog
is being closed; here is when we forward the call to the method OnClosingDialog()
.
Now, how to resize the UserControl
when the user resizes the FileOpenDialog
; this is done by processing the message WM_WINDOWPOSCHANGING
; here, we specify to change the size of the control relative to the FileOpenDialog
size.
As an important detail, when the OpenFileWindow
is closing, it must restore the original size as it was when we opened it, this is possible because OpenFileWindow
remembers the last position/size. If we don�t do that, every time the OpenFileDialog
is open, it will increment the size making it bigger and bigger.
Conclusion
I tested this on Windows XP and it works well, I didn�t have a chance to try on different OSs as Windows 2000/2003 or Vista, but it should work OK without problems. I don�t think it will work on Windows 95/98, because I�m setting the struct sizes only to match the WinNT OS. If you have any comments or discover a bug, let me know and I�ll update the control.
History
- Release 1.0.1 (11/14/2006)
- Forwards the
DialogResult
status to the caller.
- Code optimization.
- Uses the
SetWindowsPos
API with special flags to resize the control instead of direct size assignment to reduce flickering.
- Control resizing is now done in
WM_SIZING
; this fixes a bug when the control is not resized for the last update until the mouse button is released.
- Initial release 1.0.0 (07/14/2006)