在.NET里面,MS提供了OpenFileDialog和SaveFileDialog,但是MS暴露给我们的方法和属性很少。有的时候我们需要修改他们的外观,比如修改默认的显示风格,默认的排序,甚至要求在Dialog中浏览图片等。这个时候我们除了重做一个Dialog那么就只能依靠消息来达到我们的目的。
关于如果在Dialog中浏览图片的文章,大家可以参考下CodeProject上的http://www.codeproject.com/KB/dialog/OpenFileDialogEx.aspx 这篇文章。
那么如何来改变默认的显示方式,以及如果是Details形式的话,如何更改默认的排序属性呢,这个就是我这篇文章需要介绍的内容。
我先说一下大致的思路:
我们用Spy++看到,点击列排序的时候是LVM_SORTITEMS消息
通过MSDN我们知道,发送LVM_SORTITEMS需要同时提供排序的方法,因此要把你排序的方法放到SendMessage的参数中
在排序的方法中通过LVM_FINDITEM和LVM_GETITEMTEXT等消息获得需要排序的列
然后比较并且返回比较结果
MSDN里面有个例子,大家可以参考下: http://support.microsoft.com/kb/170884/
好了,接下来就说下我实现的方式吧,我这次主要是实现对日期列的排序,如果大家需要用到其他的列的话,请注意修改排序的代码
首先,我们需要定义两个enum,一个是用来表示默认的显示类型,一个是表示排序类型,具体代码如下
public enum FileListView { Icons = 0x7029, SmallIcons = 0x702a, List = 0x702b, Details = 0x702c, Thumbnails = 0x7031, XpThumbnails = 0x702d } public enum DialogOrderType { Ascending, Descending }
然后需要定义一些消息常量以及结构,这个没什么说的了,都是通过MSDN查到的
[StructLayout(LayoutKind.Sequential)] private struct LV_FINDINFO { public int flags; public string psz; public int lParam; public POINT pt; public int vkDirection; } [StructLayout(LayoutKind.Sequential)] private struct LV_ITEM { public int mask; public int iItem; public int iSubItem; public int state; public int stateMask; public string pszText; public int cchTextMax; public int iImage; public int lParam; public int iIndent; } [StructLayout(LayoutKind.Sequential)] private struct NMHDR { public int hwndFrom; public int idFrom; public int code; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int x; public int y; } [StructLayout(LayoutKind.Sequential)] private struct NM_LISTVIEW { public NMHDR hdr; public int iItem; public int iSubItem; public int uNewState; public int uOldState; public int uChanged; public POINT ptAction; public int lParam; } [StructLayout(LayoutKind.Sequential)] private struct HDITEM { public int mask; public int cxy; public string pszText; public int hbm; public int cchTextMax; public int fmt; public int lParam; public int iImage; public int iOrder; } public const int WM_COMMAND = 0x0111; public const int WM_ENTERIDLE = 0x0121; public const int LVFI_PARAM = 0x1; public const int LVIF_TEXT = 0x1; public const int LVM_FIRST = 0x1000; public const int LVM_FINDITEM = (LVM_FIRST + 13); public const int LVM_GETITEMTEXT = (LVM_FIRST + 45); public const int LVM_SORTITEMS = (LVM_FIRST + 48); public const int LVM_GETHEADER = (LVM_FIRST + 31); public const int LVM_SETSELECTEDCOLUMN = LVM_FIRST + 140; public const int HDI_FORMAT = 0x4; public const int HDF_LEFT = 0; public const int HDF_STRING = 0x4000; public const int HDF_SORTUP = 0x400; public const int HDF_SORTDOWN = 0x200; public const int HDM_FIRST = 0x1200; public const int HDM_SETITEM = HDM_FIRST + 12; public const int HDM_GETITEM = HDM_FIRST + 11;
接下来就是API的引用,这里主要用到SendMessage和FindWindowEx
[DllImport("user32.dll", EntryPoint = "SendMessageA", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern int SendMessage(int Hdc, int Msg_Const, int wParam, ref HDITEM lParam); [DllImport("user32.dll", EntryPoint = "SendMessageA", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern int SendMessage(int Hdc, int Msg_Const, int wParam, int lParam); [DllImport("user32.dll", EntryPoint = "SendMessageA", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern int SendMessage(int Hdc, int Msg_Const, int wParam, ref LV_FINDINFO lParam); [DllImport("user32.dll", EntryPoint = "SendMessageA", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)] private static extern int SendMessage(int Hdc, int Msg_Const, int wParam, ref LV_ITEM lParam); [DllImport("user32.dll", EntryPoint="FindWindowExA", CallingConvention=CallingConvention.StdCall,CharSet=CharSet.Ansi)] private static extern int FindWindowEx(int hwndParent, int hwndChildAfter, string lpszClass, string lpszWindow);
上面这些代码我建议大家专门放到一个类里面,以后用到的所有的API方面的东西都可以从这个类里面获得
以下就是FileDialogExtender类的内容,这个类继承自我的HookWndProc类,
这个类的代码在以前写的那篇NativeWindow的文章,建议大家先去看看
http://blog.csdn.net/lovefootball/archive/2007/09/14/1784882.aspx
public class FileDialogExtender : HookWndProc { //排序用的委托 private delegate int CompareDatesDelegate(int lParam1, int lParam2, int wnd); private ApiHelp.LV_FINDINFO lvFindInfo = new ApiHelp.LV_FINDINFO(); private ApiHelp.LV_ITEM lvItem = new ApiHelp.LV_ITEM(); private int lastListViewHandle = 0; //日期数据的排序方法 private int CompareDates(int lParam1, int lParam2, int wnd) { bool order = (OrderType == DialogOrderType.Descending); DateTime dDate1; DateTime dDate2; string temp1 = GetItemDate(wnd, lParam1); string temp2 = GetItemDate(wnd, lParam2); if (temp1.IndexOf("@") >= 0 && temp2.IndexOf("@") >= 0) { dDate1 = Convert.ToDateTime(temp1.Substring(1)); dDate2 = Convert.ToDateTime(temp2.Substring(1)); } else if (temp1.IndexOf("@") >= 0 && temp2.IndexOf("@") < 0) { dDate1 = DateTime.MinValue; dDate2 = Convert.ToDateTime(temp2); } else if (temp1.IndexOf("@") < 0 && temp2.IndexOf("@") >= 0) { dDate1 = Convert.ToDateTime(temp1); dDate2 = DateTime.MinValue; } else { dDate1 = Convert.ToDateTime(temp1); dDate2 = Convert.ToDateTime(temp2); } if (order) { if (dDate1 < dDate2) { return 2; } else if (dDate1 == dDate2) { return 1; } else { return 0; } } else { if (dDate1 > dDate2) { return 2; } else if (dDate1 == dDate2) { return 1; } else { return 0; } } } //获得日期列的数据 private string GetItemDate(int wnd, int lParam) { lvFindInfo.flags = ApiHelp.LVFI_PARAM; lvFindInfo.lParam = lParam; int index = ApiHelp.SendMessage(wnd, ApiHelp.LVM_FINDITEM, -1, ref lvFindInfo); lvItem.mask = ApiHelp.LVIF_TEXT; lvItem.iSubItem = 3; lvItem.pszText = " "; lvItem.cchTextMax = 32; int result = ApiHelp.SendMessage(wnd, ApiHelp.LVM_GETITEMTEXT, index, ref lvItem); if (result > 0) { //由于Dialog的排序时区分文件和文件夹,所以自己写排序的时候也要区分一下, //这里我偷懒了,直接用大小这个列来判断是文件还是文件夹,如果大小为“”,则为文件夹 string size = GetItemSize(wnd, lParam); if (String.IsNullOrEmpty(size)) { return "@" + lvItem.pszText.ToString(); } return lvItem.pszText.ToString(); } return ""; } //获得大小列的数据 private string GetItemSize(int wnd, int lParam) { ApiHelp.LV_FINDINFO find = new ApiHelp.LV_FINDINFO(); find.flags = ApiHelp.LVFI_PARAM; find.lParam = lParam; int index = ApiHelp.SendMessage(wnd, ApiHelp.LVM_FINDITEM, -1, ref find); ApiHelp.LV_ITEM item = new ApiHelp.LV_ITEM(); item.mask = ApiHelp.LVIF_TEXT; item.iSubItem = 1; item.pszText = " "; item.cchTextMax = 32; int result = ApiHelp.SendMessage(wnd, ApiHelp.LVM_GETITEMTEXT, index, ref item); if (result > 0) { return item.pszText.Trim(); } return ""; } private FileListView view = FileListView.Details; public FileListView View { get { return view; } set { view = value; } } private DialogOrderType orderType = DialogOrderType.Descending; public DialogOrderType OrderType { get { return orderType; } set { orderType = value; } } protected override void WndProc(ref Message m) { if (m.Msg == ApiHelp.WM_ENTERIDLE) { if (lastListViewHandle == 0) { int dialogHandle = m.LParam.ToInt32(); int listviewHandle = ApiHelp.FindWindowEx(dialogHandle, 0, "SHELLDLL_DefView", ""); if (listviewHandle != 0) { //默认显示Details ApiHelp.SendMessage(listviewHandle, ApiHelp.WM_COMMAND, (int)View, 0); } if (View == FileListView.Details) { int listHandle = ApiHelp.FindWindowEx(listviewHandle, 0, "SysListView32", "FolderView"); if (listHandle != 0) { //得到Header int hHeader = ApiHelp.SendMessage(listHandle, ApiHelp.LVM_GETHEADER, 0, 0); //去掉名称列的图标 ApiHelp.HDITEM hdName = new ApiHelp.HDITEM(); hdName.mask = ApiHelp.HDI_FORMAT; ApiHelp.SendMessage(hHeader, ApiHelp.HDM_GETITEM, 0, ref hdName); hdName.fmt ^= ApiHelp.HDF_SORTUP; ApiHelp.SendMessage(hHeader, ApiHelp.HDM_SETITEM, 0, ref hdName); //设置日期列为排序列,并且根据排序种类设置排序顺序 ApiHelp.HDITEM hd = new ApiHelp.HDITEM(); hd.mask = ApiHelp.HDI_FORMAT; hd.fmt = ApiHelp.HDF_LEFT | ApiHelp.HDF_STRING; if (OrderType == DialogOrderType.Descending) { hd.fmt |= ApiHelp.HDF_SORTDOWN; } else { hd.fmt |= ApiHelp.HDF_SORTUP; } ApiHelp.SendMessage(hHeader, ApiHelp.HDM_SETITEM, 3, ref hd); ApiHelp.SendMessage(listHandle, ApiHelp.LVM_SORTITEMS, listHandle, Marshal.GetFunctionPointerForDelegate(new CompareDatesDelegate(CompareDates)).ToInt32()); //设置日期列为最后的选择列 ApiHelp.SendMessage(listHandle, ApiHelp.LVM_SETSELECTEDCOLUMN, 3, 0); } } lastListViewHandle = listviewHandle; } } this.FireWndProcEvent(ref m); } }
调用的方法如下:
FileDialogExtender fd = new FileDialogExtender(); OpenFileDialog of = new OpenFileDialog(); fd.BeginHookProc(this); fd.View = FileListView.Details; of.ShowDialog(); fd.EndHookProc(this);
恩,大致的功能就实现了
大家需要注意的是设置完默认显示为Details格式的时候,他默认的是名称排序,
这个时候我们不仅要去掉名称列上的三角图标,还要改成其他列排序,
而且还需要发送LVM_SETSELECTEDCOLUMN消息,告诉对话框,我们选择的是哪个列
大家如果有兴趣的话,可以参照这个例子进行扩展
首先列的排序应该是做成属性,在外面调用的时候配置的
另外Details形式下还可以默认显示成其他的列,而不是对话框给我们的四个列
或者可以改变列的顺序,把日期弄到第一列等等
欢迎转载,请注明出处~~