WinForm的开发者们,想必对PostMessage和SendMessage两个API都非常熟悉了。下面给出PostMessage函数在C#中的两种声明形式:
代码
[DllImport(
"
user32.dll
"
, CharSet
=
CharSet.Unicode)]
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>
///
数据传递类
///
</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>
///
传递数据的辅助类
///
</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>
///
窗体基类
///
</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中,跨线程调用控件,是不被容许的,大部分同学采用的是这两种方法来解决该问题:
//方法一:不进行跨线程安全检查
//方法二:使用委托
Invoke(delegate{……});
方法一的好处和坏处,我说不出个所以然来,但这个办法肯定不妙,比尔大叔都不建议这么做。方法二的缺点我是明白的,一是效率的问题,会导致数据在线程间的封送;是如果这个Invoke方法需要频繁的调用(每秒50次以上,我在做射频读卡器软件时遇到过),就会出问题。
这时PostMessage就发挥作用了,把后台线程的操作结果通过Windows消息发给窗体,窗体收到消息后提取数据并按照你需要的方式显示,线程安全的问题以及效率的问题解决了。
下面给出场景一的演示代码,首先定义一个资源请求的承载类或结构,该类用于承载长时间操作的任务,可以简单如下:
代码
///
<summary>
///
资源请求类
///
</summary>
public
class
AskDataTask
{
///
<summary>
///
发起请求的控件句柄
///
</summary>
public
IntPtr askSource;
///
<summary>
///
模拟需要请求的资源ID
///
</summary>
public
string
dataName;
}
一个简单的后台线程处理界面请求的类看起来应该是这样子的:
代码
class
Server
{
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();
}
}
}
如果我们有某一个窗体继承自上面的BaseForm,那么就可以这样向服务器请求资源:
代码
public
partial
class
TestFrom : BaseForm
{
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文件,可以在这里获取。
希望该技巧能够对大家有所帮助,刚刚和鈡秋洁分手,没想到更有魅力的郭琴婕又送上门来了,祝大家多拿过节费!呵呵。