Click here to Skip to main content
Licence 
First Posted 24 Aug 2004
Views 157,145
Downloads 1,894
Bookmarked 53 times

Extending the save file dialog class in .NET

By Neil Davidson | 24 Aug 2004
Using the GetSaveFileName function to extend the Save File dialog class
1 vote, 4.2%
1

2
3 votes, 12.5%
3
4 votes, 16.7%
4
16 votes, 66.7%
5
4.44/5 - 24 votes
1 removed
μ 4.08, σa 1.78 [?]

Introduction

Sometimes programming is like doing a jigsaw puzzle with a missing corner.

I work at Red Gate Software, the company that produces SQL Compare, which is written in C#. Although C# and the .NET framework enable us to write good code quickly, the combination has its drawbacks. More often than not I find that even though .NET covers 95 percent of the native Windows API, the five percent that’s missing is the five percent I need. This article describes one of these situations.

C# is very good at handling international character support. This means that SQL Compare can handle databases that use diverse character sets. The problem comes when we need to get the T-SQL scripts out of SQL Compare and into other third-party tools.

Tangled web of encoding

There are a number of different ways of saving Unicode data to disk. ASCII encoding doesn’t handle international characters at all – it will only cope with seven-bit values from 0 to 127. UTF-8 encoding is the same as ANSI encoding for seven-bit characters such as A, B, C and D. Other, more exotic, characters are stored as two-, three- or four-byte combinations. Unicode, or UTF-16, encoding normally stores each character as two bytes, although it can occasionally use four bytes. In addition, each encoding type allows a preamble. This consists of two (for UTF-16) or three (UTF-8) bytes written at the start of a file to indicate the encoding of the file.

Not all tools support all encodings. SQL Query Analyzer, for example, supports UTF-16, but not UTF-8 encoded files; if you ask it to open a UTF-8 file it will treat it as ASCII and treat a two-byte international character as two separate characters. If, however, you save files as UTF-16, then editors that are not Unicode-aware will display each two-byte character as two single characters, l i k e t h i s.

Simple solution?

The lack of universal support means that we need to let our users choose the type of encoding they want when saving files to disk. This is where the tricky part starts. .NET provides a SaveFileDialog class that you can normally use to get a filename. Unfortunately, this dialog does not provide a way of asking the user for an encoding. I thought adding this functionality would be a simple matter of inheriting from the SaveFileDialog class and then adding a couple of controls to the dialog. This class is not, however, a simple Windows form. Instead, it uses the Windows GetSaveFileName function to display the save file dialog.

This made things a bit harder. I looked at the MSDN help for the SaveFileDialog class and noticed that there is a protected HookProc method. This lets you hook into the dialog creation process. You should be able to inherit from the class, intercept the appropriate windows messages, and then add the controls onto the form. Unfortunately, the SaveFileDialog class is sealed. This means that you cannot inherit from the class and override the HookProc method.

At this point I did a search through MSDN and the newsgroups. I found a couple of interesting articles by Dino Esposito and some newsgroup postings by Nicholas Paladino that suggested a good approach.

I decided to use the GetSaveFileName from the Windows API. This takes an OPENFILENAME struct. One of the fields of this struct is a hook that expects a function pointer. One of the neat things about .NET interop is the way you can use a managed C# delegate in place of an unmanaged C++ function pointer:

.
.
private delegate int OFNHookProcDelegate(int hdlg, int msg, 
                                         int wParam, int lParam);
.
.
ofn.lpfnHook = new OFNHookProcDelegate(HookProc);
.
.
GetSaveFileName(ref ofn)
.
.
private int HookProc(int hdlg, int msg, int wParam, int lParam)
{
      //function body
}
.
.

When GetSaveFileName is called, the HookProc receives notification messages from the save file dialog box.

I needed to hook into the WM_INITDIALOG message. In the hook I found the location of the standard Save as type: label and combobox and added a new label and combo box for the encoding choices using CreateWindowEx:

int fileTypeWindow=GetDlgItem(parent, 0x441);
RECT aboveRect = new RECT();
GetWindowRect(fileTypeWindow, ref aboveRect);

POINT point=new POINT();
point.X=aboveRect.Left;
point.Y=aboveRect.Bottom;
ScreenToClient(parent, ref point);

int labelHandle=CreateWindowEx(0, "STATIC", "mylabel", 
                               WS_VISIBLE | WS_CHILD | WS_TABSTOP, 
                               point.X, point.Y + 12, 200, 100, parent, 
                               0, 0, 0);
SetWindowText(labelHandle, "&Encoding:");

In addition to intercepting the WM_INITDIALOG message, I needed to intercept the WM_DESTROY and WM_NOTIFY messages to destroy the label and combo box, and to intercept the selection of a file to work out the chosen encoding.

The final dialog box looks like this:

Download the source code

You can download the complete source code for this sample by following the link at the top of this article. Although the sample doesn’t provide all the functionality of the SaveFileDialog class, it should provide a good starting point for you to implement the functionality you need.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Neil Davidson



United Kingdom United Kingdom

Member
Neil Davidson is technical director of Red Gate Software (www.red-gate.com), a company that provides simple tools for Microsoft developers, testers and DBAs. He can be reached at neil.davidson@red-gate.com.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralNMHDR.Code has changed in Windows Vista Pinmembermalac13:06 30 Jul '10  
GeneralGet File Name control in SaveAs dialog PinmemberSameer Abhang21:49 10 Mar '10  
GeneralRe: Get File Name control in SaveAs dialog Pinmembermalac12:58 30 Jul '10  
GeneralRe: Get File Name control in SaveAs dialog Pinmemberdmihailescu7:36 17 Nov '10  
QuestionHow css can be applied in files stored in Memory Pinmembersushilabhanvar23:16 1 Apr '09  
GeneralThank you - I have a question Pinmemberloumarco6:14 8 Dec '08  
GeneralApplying this to all applications Pinmemberscoulton8913:24 12 Oct '08  
Questionhow can i find a dialog item id needed by GetDlgItem()? PinmemberAjithevn22:58 4 Mar '08  
GeneralAwesome Pinmemberviperjason14:08 29 Feb '08  
QuestionGood stuff, I have a question PinmemberZardoz3:48 17 Nov '07  
QuestionAny resource usage considerations? Pinmemberflipdoubt5:35 9 Nov '07  
QuestionRe: Any resource usage considerations? Pinmemberflipdoubt6:13 9 Nov '07  
Generalresizable file dialog Pinmembertsaik9923:07 17 Jun '07  
GeneralRe: resizable file dialog PinmemberGiorgi Dalakishvili23:39 17 Jun '07  
GeneralDialog items Pinmemberjuliusf6:49 15 May '07  
GeneralReplase "|" with #0, not #92 PinmemberGeorge Birbilis11:02 25 Mar '07  
GeneralVB.net version with FileOK event [modified] PinmemberGeorge Birbilis12:31 20 Mar '07  
'20070325: George Birbilis (birbilis@kagi.com)
'Converted sample at http://www.codeproject.com/csharp/GetSaveFileName.asp from C# to VB.net
'Added "Encoding" property
'Added more "EncodingTypes" and respective dropdown strings
'Fixed filter to use "Chr" instead of "ChrW" and convert "|" [Chr(124)] to Chr(0) instead of ChrW(92)
 
' Example of use:
'
'   Dim ofd As SaveFileDialogWithEncoding = New SaveFileDialogWithEncoding
'   ofd.DefaultExt = "sql"
'   ofd.EncodingType = EncodingTypes.UTF8
'   ofd.Filter = "SQL files (*.sql)|*.sql|All files (*.*)|*.*"
'   If (ofd.ShowDialog = DialogResult.OK) Then
'   MessageBox.Show(String.Format("Name={0}, Encoding={1}", ofd.FileName, ofd.EncodingType))
'   End If
 
Imports System.Text
Imports System.Text.Encoding
Imports System.Drawing
Imports System.Collections
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Data
Imports System.Runtime.InteropServices
 
Public Class SaveFileDialogWithEncoding
Inherits Component
 
#Region "Enums"
 
'note the order of these is important
Public Enum EncodingTypes
   ANSI = 0
   UTF8
   UTF16
   UTF7
   UTF32
   UNKNOWN
End Enum
 
#End Region
 
#Region "Fields"
 
Protected Encodings As System.Text.Encoding() = New System.Text.Encoding() {ASCII, UTF8, Unicode, UTF7, UTF32}
 
Private m_LabelHandle As Integer = 0
Private m_ComboHandle As Integer = 0
Private m_Filter As String = ""
Private m_DefaultExt As String = ""
Private m_FileName As String = ""
Private m_EncodingType As EncodingTypes
Private m_ActiveScreen As Screen
 
#End Region
 
#Region "Constants"
 
Private Const OFN_ENABLEHOOK As Integer = 32
Private Const OFN_EXPLORER As Integer = 524288
Private Const OFN_FILEMUSTEXIST As Integer = 4096
Private Const OFN_HIDEREADONLY As Integer = 4
Private Const OFN_CREATEPROMPT As Integer = 8192
Private Const OFN_NOTESTFILECREATE As Integer = 65536
Private Const OFN_OVERWRITEPROMPT As Integer = 2
Private Const OFN_PATHMUSTEXIST As Integer = 2048
Private Const SWP_NOSIZE As Integer = 1
Private Const SWP_NOMOVE As Integer = 2
Private Const SWP_NOZORDER As Integer = 4
Private Const WM_INITDIALOG As Integer = 272
Private Const WM_DESTROY As Integer = 2
Private Const WM_SETFONT As Integer = 48
Private Const WM_GETFONT As Integer = 49
Private Const CBS_DROPDOWNLIST As Integer = 3
Private Const CBS_HASSTRINGS As Integer = 512
Private Const CB_ADDSTRING As Integer = 323
Private Const CB_SETCURSEL As Integer = 334
Private Const CB_GETCURSEL As Integer = 327
Private Const WS_VISIBLE As UInteger = 268435456
Private Const WS_CHILD As UInteger = 1073741824
Private Const WS_TABSTOP As UInteger = 65536
Private Const CDN_FILEOK As Integer = -606
Private Const WM_NOTIFY As Integer = 78
 
#End Region
 
#Region "Properties"
 
Public Property DefaultExt() As String
   Get
   Return m_DefaultExt
   End Get
   Set(ByVal value As String)
   m_DefaultExt = value
   End Set
End Property
 
Public Property Filter() As String
   Get
   Return m_Filter
   End Get
   Set(ByVal value As String)
   m_Filter = value
   End Set
End Property
 
Public Property FileName() As String
   Get
   Return m_FileName
   End Get
   Set(ByVal value As String)
   m_FileName = value
   End Set
End Property
 
Public Property EncodingType() As EncodingTypes
   Get
   Return m_EncodingType
   End Get
   Set(ByVal value As EncodingTypes)
   m_EncodingType = value
   End Set
End Property
 
Public Property Encoding() As Encoding
   Get
   Return Encodings(m_EncodingType)
   End Get
   Set(ByVal value As Encoding)
   Dim i As Integer = 0
   For Each enc As Encoding In Encodings
      If enc.Equals(value) Then
      m_EncodingType = CType(i, EncodingTypes)
      Exit Property
      End If
   Next enc
   m_EncodingType = EncodingTypes.UNKNOWN
   End Set
End Property
 
#End Region
 
#Region "Methods"
 
<DllImport("Comdlg32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
Private Shared Function GetSaveFileName(ByRef lpofn As OPENFILENAME) As Boolean
End Function
 
<DllImport("Comdlg32.dll")> _
Private Shared Function CommDlgExtendedError() As Integer
End Function
 
<DllImport("user32.dll")> _
Private Shared Function SetWindowPos(ByVal hWnd As Integer, ByVal hWndInsertAfter As Integer, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal uFlags As UInteger) As Boolean
End Function
 
<DllImport("user32.dll")> _
Private Shared Function GetWindowRect(ByVal hWnd As Integer, ByRef lpRect As RECT) As Boolean
End Function
 
<DllImport("user32.dll")> _
Private Shared Function GetParent(ByVal hWnd As Integer) As Integer
End Function
 
<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Private Shared Function SetWindowText(ByVal hWnd As Integer, ByVal lpString As String) As Boolean
End Function
 
<DllImport("user32.dll")> _
Private Overloads Shared Function SendMessage(ByVal hWnd As Integer, ByVal Msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
End Function
 
<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Private Overloads Shared Function SendMessage(ByVal hWnd As Integer, ByVal Msg As Integer, ByVal wParam As Integer, ByVal lParam As String) As Integer
End Function
 
<DllImport("user32.dll")> _
Private Shared Function DestroyWindow(ByVal hwnd As Integer) As Boolean
End Function
 
<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Private Shared Function GetDlgItem(ByVal hDlg As Integer, ByVal nIDDlgItem As Integer) As Integer
End Function
 
<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Private Shared Function CreateWindowEx(ByVal dwExStyle As Integer, ByVal lpClassName As String, ByVal lpWindowName As String, ByVal dwStyle As UInteger, ByVal x As Integer, ByVal y As Integer, ByVal nWidth As Integer, ByVal nHeight As Integer, ByVal hWndParent As Integer, ByVal hMenu As Integer, ByVal hInstance As Integer, ByVal lpParam As Integer) As Integer
End Function
 
<DllImport("user32.dll")> _
Private Shared Function ScreenToClient(ByVal hWnd As Integer, ByRef lpPoint As POINT) As Boolean
End Function
 
Private Function HookProc(ByVal hdlg As Integer, ByVal msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
   Select Case (msg)
   Case WM_INITDIALOG
      'we need to centre the dialog
      Dim sr As Rectangle = m_ActiveScreen.Bounds
      Dim cr As RECT = New RECT
      Dim parent As Integer = GetParent(hdlg)
      GetWindowRect(parent, cr)
      Dim x As Integer = CInt((sr.Right + (sr.Left - (cr.Right - cr.Left))) / 2)
      Dim y As Integer = CInt((sr.Bottom + (sr.Top - (cr.Bottom - cr.Top))) / 2)
      SetWindowPos(parent, 0, x, y, (cr.Right - cr.Left), ((cr.Bottom - cr.Top) + 32), SWP_NOZORDER)
      'we need to find the label to position our new label under
      Dim fileTypeWindow As Integer = GetDlgItem(parent, 1089)
      Dim aboveRect As RECT = New RECT
      GetWindowRect(fileTypeWindow, aboveRect)
      'now convert the label's screen co-ordinates to client co-ordinates
      Dim point As POINT = New POINT
      point.X = aboveRect.Left
      point.Y = aboveRect.Bottom
      ScreenToClient(parent, point)
      'create the label
      Dim labelHandle As Integer = CreateWindowEx(0, "STATIC", "mylabel", (WS_VISIBLE _
                              Or (WS_CHILD Or WS_TABSTOP)), point.X, (point.Y + 12), 200, 100, parent, 0, 0, 0)
      SetWindowText(labelHandle, "&Encoding:") 'should show with same encoding as system dialog or not show label at all
      Dim fontHandle As Integer = SendMessage(fileTypeWindow, WM_GETFONT, 0, 0)
      SendMessage(labelHandle, WM_SETFONT, fontHandle, 0)
      'we now need to find the combo-box to position the new combo-box under
      Dim fileComboWindow As Integer = GetDlgItem(parent, 1136)
      aboveRect = New RECT
      GetWindowRect(fileComboWindow, aboveRect)
      point = New POINT
      point.X = aboveRect.Left
      point.Y = aboveRect.Bottom
      ScreenToClient(parent, point)
      Dim rightPoint As POINT = New POINT
      rightPoint.X = aboveRect.Right
      rightPoint.Y = aboveRect.Top
      ScreenToClient(parent, rightPoint)
      'we create the new combobox
      Dim comboHandle As Integer = CreateWindowEx(0, "ComboBox", "mycombobox", (WS_VISIBLE _
                              Or (WS_CHILD _
                              Or (CBS_HASSTRINGS _
                              Or (CBS_DROPDOWNLIST Or WS_TABSTOP)))), point.X, (point.Y + 8), (rightPoint.X - point.X), 100, parent, 0, 0, 0)
      SendMessage(comboHandle, WM_SETFONT, fontHandle, 0)
      'and add the encodings we want to offer
      SendMessage(comboHandle, CB_ADDSTRING, 0, "ANSI")
      SendMessage(comboHandle, CB_ADDSTRING, 0, "Unicode (UTF-8)")
      SendMessage(comboHandle, CB_ADDSTRING, 0, "Unicode (UTF-16)")
      SendMessage(comboHandle, CB_ADDSTRING, 0, "Unicode (UTF-7)")
      SendMessage(comboHandle, CB_ADDSTRING, 0, "Unicode (UTF-32)")
      SendMessage(comboHandle, CB_SETCURSEL, CType(m_EncodingType, Integer), 0)
      'remember the handles of the controls we have created so we can destroy them after
      m_LabelHandle = labelHandle
      m_ComboHandle = comboHandle
   Case WM_DESTROY
      'destroy the handles we have created
      If (m_ComboHandle <> 0) Then
      DestroyWindow(m_ComboHandle)
      End If
      If (m_LabelHandle <> 0) Then
      DestroyWindow(m_LabelHandle)
      End If
   Case WM_NOTIFY
      'we need to intercept the CDN_FILEOK message
      'which is sent when the user selects a filename
      Dim nmhdr As NMHDR = CType(Marshal.PtrToStructure(New IntPtr(lParam), GetType(NMHDR)), NMHDR)
      If (nmhdr.Code = CDN_FILEOK) Then
      'a file has been selected
      'we need to get the encoding
      m_EncodingType = CType(SendMessage(m_ComboHandle, CB_GETCURSEL, 0, 0), EncodingTypes)
      End If
   End Select
   Return 0
End Function
 
Public Function ShowDialog() As DialogResult
   'set up the struct and populate it
   Dim ofn As OPENFILENAME = New OPENFILENAME
   ofn.lStructSize = Marshal.SizeOf(ofn)
   ofn.lpstrFilter = (m_Filter.Replace("|", Microsoft.VisualBasic.Chr(0)) + Microsoft.VisualBasic.Chr(0))
   ofn.lpstrFile = (m_FileName + New String(Microsoft.VisualBasic.Chr(32), 512))
   ofn.nMaxFile = ofn.lpstrFile.Length
   ofn.lpstrFileTitle = (System.IO.Path.GetFileName(m_FileName) + New String(Microsoft.VisualBasic.Chr(32), 512))
   ofn.nMaxFileTitle = ofn.lpstrFileTitle.Length
   ofn.lpstrTitle = "Save file as"
   ofn.lpstrDefExt = m_DefaultExt
   'position the dialog above the active window
   ofn.hwndOwner = Form.ActiveForm.Handle
   'we need to find out the active screen so the dialog box is
   'centred on the correct display
   m_ActiveScreen = Screen.FromControl(Form.ActiveForm)
   'set up some sensible flags
   ofn.Flags = (OFN_EXPLORER _
                     Or (OFN_PATHMUSTEXIST _
                     Or (OFN_NOTESTFILECREATE _
                     Or (OFN_ENABLEHOOK _
                     Or (OFN_HIDEREADONLY Or OFN_OVERWRITEPROMPT)))))
   'this is where the hook is set. Note that we can use a C# delegate in place of a C function pointer
   ofn.lpfnHook = New OFNHookProcDelegate(AddressOf HookProc)
   'if we're running on Windows 98/ME then the struct is smaller
   If (System.Environment.OSVersion.Platform <> PlatformID.Win32NT) Then
   ofn.lStructSize = (ofn.lStructSize - 12)
   End If
   'show the dialog
   If Not GetSaveFileName(ofn) Then
   Dim ret As Integer = CommDlgExtendedError()
   If (ret <> 0) Then
      Throw New ApplicationException(("Couldn't show file open dialog - " + ret.ToString))
   End If
   Return DialogResult.Cancel
   End If
 
   'Birb-start
   Dim oldFilename As String = m_FileName
   m_FileName = ofn.lpstrFile
   Dim cancelCheck As New CancelEventArgs()
   RaiseEvent FileOK(Me, cancelCheck)
   If cancelCheck.Cancel Then
   m_FileName = oldFilename 'restore filename since dialog was canceled
   Return DialogResult.Cancel
   Else
   Return DialogResult.OK
   End If
   'Birb-end
End Function
 
Public Delegate Function OFNHookProcDelegate(ByVal hdlg As Integer, ByVal msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
 
#End Region
 
#Region "Structures"
 
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
Private Structure OPENFILENAME
   Public lStructSize As Integer
   Public hwndOwner As IntPtr
   Public hInstance As Integer
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpstrFilter As String
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpstrCustomFilter As String
   Public nMaxCustFilter As Integer
   Public nFilterIndex As Integer
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpstrFile As String
   Public nMaxFile As Integer
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpstrFileTitle As String
   Public nMaxFileTitle As Integer
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpstrInitialDir As String
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpstrTitle As String
   Public Flags As Integer
   Public nFileOffset As Short
   Public nFileExtension As Short
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpstrDefExt As String
   Public lCustData As Integer
   Public lpfnHook As OFNHookProcDelegate
   <MarshalAs(UnmanagedType.LPTStr)> _
Public lpTemplateName As String
   'only if on nt 5.0 or higher
   Public pvReserved As Integer
   Public dwReserved As Integer
   Public FlagsEx As Integer
End Structure
 
Private Structure RECT
   Public Left As Integer
   Public Top As Integer
   Public Right As Integer
   Public Bottom As Integer
End Structure
 
Private Structure POINT
   Public X As Integer
   Public Y As Integer
End Structure
 
Private Structure NMHDR
   Public HwndFrom As Integer
   Public IdFrom As Integer
   Public Code As Integer
End Structure
 
#End Region
 
#Region "Events"
 
Public Event FileOK(ByVal sender As System.Object, ByVal e As System.ComponentModel.CancelEventArgs) 'Birb
 
#End Region
 
End Class

 
http://www.kagi.com/birbilis
GeneralGood stuff, but I have a question. Pinmemberroybrew6:02 27 Oct '06  
GeneralRe: Good stuff, but I have a question. Pinmemberroybrew8:40 27 Oct '06  
Questionvery nice Pinmemberchandu.sanka2:30 8 Sep '06  
GeneralControl Id PinmemberKynanHaas18:16 31 May '06  
GeneralRe: Control Id PinmemberGeorge Birbilis7:34 20 Mar '07  
QuestionC# Control in dialog PinmemberOlaf Gramkow6:42 26 May '06  
AnswerRe: C# Control in dialog PinmemberStuart Carnie20:57 7 Feb '07  
QuestionRe: C# Control in dialog Pinmemberflipdoubt5:24 27 Oct '07  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web01 | 2.5.120210.1 | Last Updated 25 Aug 2004
Article Copyright 2004 by Neil Davidson
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid