备注:Rimi是我们用的一个分布式机制。
在进行设备树(也就是一个CTreeCtrl控件)更新修改的时候,遇到了一个比较bug的问题。
为了提供更好的用户体验,甲方希望设备树更新之后滚动条位置能够保持与更新前一致。设备树的更新过程是这样的:更新消息来自Rimi的通知机制,类似于函数回调,客户端在收到消息之后调用Rimi的对象方法来获取新的设备树信息,然后更新树。乍看之下,要完成这个修改好像很简单,只要更新前先记录滚动条的滚动位置,更新后还原位置,如果更新后滚动条滚动范围变化了还要微调一下位置,逻辑上来讲就这么几个步骤。
我一开始也是按照这样的思路,GetScrollPos()获取当前滚动条的滚动位置,然后更新树(先删除所有节点再逐个添加,其他省略...),GetScrollRange()获得新的滚动范围,最后SetScrollPos()将旧的位置与滚动范围最大值中最小的一个设回去(这里用到的ScrollBar是CTreeCtrl自动产生的,注意不是两个控件,这里调用的函数都是CTreeCtrl的方法)。但实际效果是,树更新后滚动条滚到准确的位置,但树的视图到了最顶,点击一下滚动条的那个方块才能回到之前的位置。也就是说,滚动位置的更新与树的视图分离了。
之后,我一直以为是我控件的方法用错了,对着MSDN和CSDN纠结了很久。最后忍无可忍,自己写了个测试Demo,里面就一Dialog,一CTreeCTrl,树上随便加了些东西,然后又一按键,按键后会重新刷新树,再滚动到原来的位置,结果居然是对的,视图跟着滚动条的位置变化了。为了更好的模拟设备树节点增删的效果,我在按键响应上又作了处理,按一下重刷树的时候会隐藏几个节点,再按一下这些节点显示出来,滚动位置按照客户端里面的一个处理方法,结果居然也是正确的。问题变得玄乎了!
无意间发现客户端里面有个手动刷新设备树的快捷键,估计是当年pb做调试的时候留下来的。快捷键的响应直接调用更新树的函数,重刷后的显示出人意料地是对的。比较一下两种更新方式的过程:
Rimi: 通知到来—>更新树(Rimi回调函数,Rimi自己维护了一个线程池,远程调用在被调用端的发起者都是Rimi自己的线程)
快捷键: 按键响应—>更新树(MFC消息处理函数)
更新树所用到的是同一个函数,但调用者却是不同的。因为Rimi用了boost::function,那我也在按键响应的时候对要调的函数用function来包装一下,造成两者在调用栈上调用的函数、顺序大部分是一致的,只有最底层不同,一边是Rimi,一边是MFC消息传递。
后来jianhao说,以前在Rimi的回调函数里面调Rimi对象的方法出过问题,然后我又顺道回忆起之前zxb在Rimi函数(还是对象方法)里面调system()也有问题。
难道说Rimi线程就是“万恶之源”?好吧,我把更新代码移到另外一个线程里面,Rimi回调的时候唤醒更新线程,更新后视图还是不能跟着滚动位置变;将快捷键的响应也修改一下,自己不作更新,也是唤醒更新线程,这个方法也变得不灵了,囧!这可以说明问题跟Rimi线程无关。
难道说线程调用才是“万恶之源”?把之前做的那个Demo小改了一把,线程做刷新,按键响应只唤醒更新线程,果然不灵了!上网google了一把,关键字“mfc 线程 操作控件”,首先映入眼帘的是《MFC中跨线程操作控件会不会出现像C#中的异常问题?》。这时候我也不关心这个帖子的内容了,线程操作控件有异常是吧,那就不用线程做咯!这时候我才回想起WIN32里面有自定义消息这玩意,MFC里面给定一个消息ID,ON_MESSAGE绑定一个处理函数,PostMessage或SendMessage来发消息,然后由WIN32自己的消息循环来调用处理函数,这样应该是可以保证用非Rimi线程来更新设备树的。再一次把Demo小改了一把,按键响应Post一个自定义消息,消息处理函数做刷新,结果是对的;再改,按键响应唤醒线程,线程里面Post自定义消息,结果也是对的。
原以为是控件使用问题,又以为是Rimi不兼容问题,最后实质为MFC跨线程使用控件的问题。其实我也不清楚这是不是真正的问题,毕竟我MFC既不懂又用得少。That's all!
最后附上我的测试代码
http://www.cppblog.com/Files/neverwinter/testtree.rar