小技巧分享:使用PostMessage向窗体传递任何数据
WinForm的开发者们,想必对PostMessage和SendMessage两个API都非常熟悉了。下面给出PostMessage函数在C#中的两种声明形式:
static extern bool PostMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);
[DllImport("user32.dll ", CharSet = CharSet.Unicode)]
static extern bool PostMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
wParam参数和lParam参数是一个32位(在32位的系统中)的整型。因为C++支持指针,wParam和lParam可以是指向任意类型对象的指针(指针本质上就是整型),而C#不支持指针,所以wParam参数和lParam参数只能为int型或IntPtr型。这就限制了我们向控件传递任意的数据类型。
当然IntPtr有类似指针的作用,但我们又如何使自定义类型的对象获得IntPtr呢?当然会想到用System.Runtime.InteropServices.Marshal.StructureToPtr()方法,使用该方法,不但有诸多限制,而且效率极为低下(因为需要额外分配内存和进行内存拷贝)。
今天我和大家分享一个“曲线救国”的方法和该方法的一些使用场景。
首先定义一个用于承载传递数据的类或结构,可以如下:
/// 数据传递类
/// </summary>
public class DataTransfer
{
/// <summary>
/// 传递的字符串
/// </summary>
public string transferMessage;
/// <summary>
/// 传递的其他任意类型的数据
/// </summary>
public object obj;
}
您可以自定义DataTransfer类来承载你需要传递的数据,里面的transferMessage和obj字段就是你要传递的实际数据。
定义一个类型为Dictionary<int, DataTransfer>的集合对象datas,当你需要给某控件传递数据时,先生成一个DataTransfer对象value,把value加入到datas集合中,则会获得从datas中提取该value的数据键key(关键是该key是整型的),调用PostMessage把key赋值给wParam参数或lParam参数发送消息到窗体,窗体收到消息后用key来datas中提取数据,再把datas中该键值对删除。这样貌似就达到了利用 PostMessage函数向控件发送任意类型数据的目的。当然,我们可以定义一个传递数据的辅助类,该辅助类看起来应该像是这样子的:
/// 传递数据的辅助类
/// </summary>
class TransferDataHelper
{
/// <summary>
/// 自定义消息号
/// </summary>
public const int User_Message = 0x401;
/// <summary>
/// 数据集合
/// </summary>
static Dictionary<int, DataTransfer> datas;
/// <summary>
/// 线程同步对象
/// </summary>
static object objLock;
/// <summary>
/// 数据键,用于标记数据
/// </summary>
static int dataKey=0;
[DllImport("user32.dll ", CharSet = CharSet.Unicode)]
private static extern bool PostMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);
static TransferDataHelper()
{
datas = new Dictionary<int, DataTransfer>();
objLock = new object();
}
/// <summary>
/// 传递数据
/// </summary>
/// <param name="value"></param>
/// <param name="formHandle"></param>
public static void TansferData(DataTransfer value,IntPtr formHandle)
{
lock (objLock)
{
dataKey++;
datas.Add(dataKey, value);
PostMessage(formHandle, User_Message, dataKey, 0);
}
}
/// <summary>
/// 提取数据
/// </summary>
/// <param name="pickDataKey"></param>
/// <returns></returns>
public static DataTransfer GetData(int pickDataKey)
{
lock(objLock)
{
DataTransfer value = datas[pickDataKey];
datas.Remove(pickDataKey);
return value;
}
}
}
使用该助手类,使用TansferData方法向控件发送数据,控件收到消息后使用GetData方法提取数据。
下面给出窗体如何使用该助手类的演示代码:
/// 窗体基类
/// </summary>
public partial class BaseForm : Form
{
public BaseForm()
{
InitializeComponent();
}
protected virtual void ReceiveData(DataTransfer value)
{
}
protected override void WndProc(ref Message m)
{
// 若是自定义消息号,则利用WParam提取数据内容,并调用ReceiveData处理数据
if (m.Msg == TransferDataHelper.User_Message)
{
this.ReceiveData(TransferDataHelper.GetData(m.WParam.ToInt32()));
return;
}
base.WndProc(ref m);
}
}
原理介绍完了,巨简单吧,呵呵!技巧虽小,但用处却多,下面就给出两个应用场景。
场景一:
这个场景最常见,窗体上一个交互动作,需要去向后台线程提交一项长时间操作的任务,例如海量的数据查询、复杂的数据计算、长时间的IO操作……,总之,为了不让界面假死,该任务必须由后台线程去做。后台线程完成后,把任务结果返回给窗体来显示。这时我们该如何把返回的数据给窗体并在窗体上正确的显示呢?
我们知道, 在frameWork2.0中,跨线程调用控件,是不被容许的,大部分同学采用的是这两种方法来解决该问题:
//方法一:不进行跨线程安全检查
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false; |
/// 资源请求类
/// </summary>
public class AskDataTask
{
/// <summary>
/// 发起请求的控件句柄
/// </summary>
public IntPtr askSource;
/// <summary>
/// 模拟需要请求的资源ID
/// </summary>
public string dataName;
}
一个简单的后台线程处理界面请求的类看起来应该是这样子的:
{
private static Server instance;
/// <summary>
/// 唯一实例
/// </summary>
public static Server Instance
{
get {
if (instance == null)
instance = new Server();
return instance;
}
}
/// <summary>
/// 线程通知对象
/// </summary>
AutoResetEvent autoReset;
/// <summary>
/// 工作线程
/// </summary>
Thread threadWork;
/// <summary>
/// 工作任务队列
/// </summary>
Queue<AskDataTask> tasks;
public Server()
{
this.autoReset = new AutoResetEvent(false);
this.tasks = new Queue<AskDataTask>();
this.threadWork = new Thread(Work);
this.threadWork.Start();
}
private void Work()
{
while (true)
{
AskDataTask task=null;
if (this.tasks.Count == 0)
autoReset.WaitOne();
else
task = tasks.Dequeue();
if (task == null) continue;
//模拟长时间的操作
Thread.Sleep(1000);
//生成一个数据传递对象,回发给窗体。
DataTransfer data = new DataTransfer();
data.transferMessage = "来自服务器的数据";
//模拟获得的数据
data.obj = DateTime.Now.Ticks;
TransferDataHelper.TansferData(data, task.askSource);
}
}
/// <summary>
/// 增加数据请求任务
/// </summary>
/// <param name="task"></param>
public void AddTask(AskDataTask task)
{
lock (tasks)
{
tasks.Enqueue(task);
autoReset.Set();
}
}
}
{
public TestFrom()
{
InitializeComponent();
}
/// <summary>
/// 模拟长时间的资源请求操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_askData_Click(object sender, EventArgs e)
{
AskDataTask task = new AskDataTask();
task.askSource = this.Handle;
task.dataName = "需要获得XXXX资源";
Server.Instance.AddTask(task);
}
protected override void ReceiveData(DataTransfer value)
{
base.ReceiveData(value);
MessageBox.Show(value.transferMessage + ": " + value.obj.ToString());
}
/// <summary>
/// 模拟传递数据给其他窗体
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_SendData_Click(object sender, EventArgs e)
{
TestFrom otherForm = new TestFrom();
otherForm.Text = "其他窗体";
otherForm.StartPosition = FormStartPosition.CenterScreen;
otherForm.Show();
DataTransfer message = new DataTransfer();
message.transferMessage = "来自TestForm的问候";
message.obj = "Hello!";
TransferDataHelper.TansferData(message, this.Handle);
}
}
场景二:
任意窗体和窗体间传递数据,这个也是一个很常见的场景,上面代码中也实现了该场景。
如果需要Demo文件,可以在这里获取。
希望该技巧能够对大家有所帮助,刚刚和鈡秋洁分手,没想到更有魅力的郭琴婕又送上门来了,祝大家多拿过节费!呵呵。