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)
{
}
.
.
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.