topameng's profileQuake3 启示录PhotosBlogListsMore Tools Help

Blog


    March 18

    _beginthread还是CreateThread

    http://www.diybl.com/ 
    _beginthread还是CreateThread
    发表于 2006-01-28 01:56 AM 作者: polyrandom
    程序员对于Windows程序中应该用_beginthread还是CreateThread来创建线程,一直有所争论。本文将从对CRT源代码出发探讨这个问题。

    I. 起因

    今天一个朋友问我程序中究竟应该使用_beginthread还是CreateThread,并且告诉我如果使用不当可能会有内存泄漏。其实我过去对这个问题也是一知半解,为了对朋友负责,专门翻阅了一下VC的运行库(CRT)源代码,终于找到了答案。

    II. CRT

    CRT(C/C++ Runtime Library)是支持C/C++运行的一系列函数和代码的总称。虽然没有一个很精确的定义,但是可以知道,你的main就是它负责调用的,你平时调用的诸如strlen、strtok、time、atoi之类的函数也是它提供的。我们以Microsoft Visual.NET 2003中所附带的CRT为例。假设你的.NET 2003安装在C:\Program Files\Microsoft Visual Studio .NET 2003中,那么CRT的源代码就在C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\crt\src中。既然有了这些实现的源代码,我们就可以找到一切解释了。

    III. _beginthread/_endthread

    这个函数究竟做了什么呢?它的代码在thread.c中。阅读代码,可以看到它最终也是通过CreateThread来创建线程的,主要区别在于,它先分配了一个_tiddata,并且调用了_initptd来初始化这个分配了的指针。而这个指针最后会被传递到CRT的线程包装函数 _threadstart中,在那里会把这个指针作为一个TLS(Thread Local Storage)保存起来。然后_threadstart会调用我们传入的线程函数,并且在那个函数退出后调用_endthread。这里也可以看到, _threadstart用一个__try/__except块把我们的函数包了起来,并且在发生异常的时候,调用exit退出。(_threadstart和endthread的代码都在thread.c中)
    这个_tiddata是一个什么样的结构呢?它在mtdll.h中定义,它的成员被很多CRT函数所用到,譬如int _terrno,这是这个线程中的错误标志;char* _token,strtok以来这个变量记录跨函数调用的信息,...。
    那么_endthread又做了些什么呢?除了调用浮点的清除代码以外,它还调用了_freeptd来释放和这个线程相关的tiddata。也就是说,在 _beginthread里面分配的这块内存,以及在线程运行过程中其它CRT函数中分配并且记录在这个内存结构中的内存,在这里被释放了。
    通过上面的代码,我们可以看到,如果我使用_beginthread函数创建了线程,它会为我创建好CRT函数需要的一切,并且最后无需我操心,就可以把清除工作做得很好,可能唯一需要注意的就是,如果需要提前终止线程,最好是调用_endthread或者是返回,而不要调用ExitThread,因为这可能造成内存释放不完全。同时我们也可以看出,如果我们用CreateThread函数创建了线程,并且不对C运行库进行调用(包括任何间接调用),就不必担心什么问题了。

    IV. CreateThread和CRT

    或许有人会说,我用CreateThread创建线程以后,我也调用了C运行库函数,并且也使用ExitThread退出了,可是我的程序运行得好好的,既没有因为CRT没有初始化而崩溃,也没有因为忘记调用 _endthread而发生内存泄漏,这是为什么呢,让我们继续我们的CRT之旅。
    假设我用CreateThread创建了一个线程,我调用 strtok函数来进行字符串处理,这个函数肯定是需要某些额外的运行时支持的。strtok的源代码在strtok.c中。从代码可见,在多线程情况下,strtok的第一句有效代码就是_ptiddata ptd = _getptd(),它通过这个来获得当前的ptd。可是我们并没有通过_beginthread来创建ptd,那么一定是_getptd捣鬼了。打开 tidtable.c,可以看到_getptd的实现,果然,它先尝试获得当前的ptd,如果不能,就重新创建一个,因此,后续的CRT调用就安全了。可是这块ptd最终又是谁释放的呢?打开dllcrt0.c,可以看到一个DllMain函数。在VC中,CRT既可以作为一个动态链接库和主程序链接,也可以作为一个静态库和主程序链接,这个在Project Setting->Code Generations里面可以选。当CRT作为DLL链接到主程序时,DllMain就是CRT DLL的入口。Windows的DllMain可以由四种原因调用:Process Attach/Process Detach/Thread Attach/Thread Detach,最后一个,也就是当线程函数退出后但是线程还没有销毁前,会在这个线程的上下文中用Thread Detach调用DllMain,这里,CRT做了一个_freeptd(NULL),也就是说,如果有ptd,就free掉。所以说,恰巧没有发生内存泄漏是因为你用的是动态链接的CRT。
    于是我们得出了一个更精确的结论,如果我没有使用那些会使用_getptd的CRT函数,使用CreateThread就是安全的。

    V. 使用ptd的函数

    那么,究竟那些函数使用了_getptd呢?很多!在CRT目录下搜索_getptd,你会发觉很多意想不到的函数都用到了它,除了strtok、 rand这类需要保持状态的,还有所有的字符串相关函数,因为它们要用到ptd中的locale信息;所有的mbcs函数,因为它们要用到ptd中的 mbcs信息,...。

    VI. 测试代码

    下面是一段测试代码(leaker中用到了atoi,它需要ptd):

    代码: #include <windows.h>
    #include <process.h>
    #include <iostream>
    #include <CRTDBG.H>

    volatile bool threadStarted = false;

    void leaker()
    {
    std::cout << atoi( "0" ) << std::endl;
    }

    DWORD __stdcall CreateThreadFunc( LPVOID )
    {
    leaker();
    threadStarted = false;
    return 0;
    }

    DWORD __stdcall CreateThreadFuncWithEndThread( LPVOID )
    {
    leaker();
    threadStarted = false;
    _endthread();
    return 0;
    }

    void __cdecl beginThreadFunc( LPVOID )
    {
    leaker();
    threadStarted = false;
    }

    int main()
    {
    for(;;)
    {
    while( threadStarted )
    Sleep( 5 );
    threadStarted = true;
    // _beginthread( beginThreadFunc, 0, 0 );//1
    CreateThread( NULL, 0, CreateThreadFunc, 0, 0, 0 );//2
    // CreateThread( NULL, 0, CreateThreadFuncWithEndThread, 0, 0, 0 );//3
    }
    return 0;
    }
    如果你用VC的多线程+静态链接CRT(/MT或者/MTD)选项去编译这个程序,并且尝试打开1、2、3之中的一行,你会发觉只有2打开的情况下,程序才会发生内存泄漏(可以在任务管理器里面明显的观察到)。3之所以不会出现内存泄漏是因为主动调用了_endthread。

    VII. 总结

    如果你使用了DLL方式链接的CRT库,或者你只是一次性创建少量的线程,那么你或许可以采取鸵鸟策略,忽视这个问题。上面一节代码中第3种方法基于对 CRT库的了解,但是并不保证这是一个好的方法,因为每一个版本的VC的CRT可能都会有些改变。看来,除非你的头脑清晰到可以记住这一切,或者你可以不厌其烦的每调用一个C函数都查一下CRT代码,否则总是使用 _beginthread(或者它的兄弟_beginthreadex)是一个不错的选择。

    [后记]
    网友condor指出 本文的一个错误:在dllcrt0.c 中,DllMain的Thread Detach所释放的ptd,其实是dllcrt0.c的DllMain中的Thread Attach所创建的。也就是说,当你用CRT DLL的时候,DllMain对线程做了一切初始化/清除工作。我查看源代码,thread.c中的_threadstart函数,在设置TLS之前做了检查,这其实就是为了避免重复设置导致的内存泄漏。
    文章出处:http://www.diybl.com/course/3_program/c++/cppsl/2008831/139196.html

    这也是很多人用在了dll工程中,很难发现出现问题的原因吧! 大部分工程都是设置的 /md
    _BeginThread 使用了 TLS 线程本地存储。 createthread 无,只是sdk
    还有afxbeginthread 更好,这个MFC函数能够检查资源是否是当前线程分配的。如一个窗口句柄在一个线程创建,另一个线程使用就会出问题。用afxbeginthread 可以防止这个问题

    May 08

    什么才是真相?

    简介
         在当今的计算机中,随着电源管理技术越来越普遍地使用,一种用来获得高分辨率时钟的通用方法 —— RDTSC指令可能无法如按预期地工作了。本文建议使用一种更精确、更可靠的方法 —— QueryPerformanceCounter和QueryPerformanceFrequency两个Winodws API函数。
    背景
         自从x86 P5指令集引入以来,很多游戏开发者一直在使用RDTSC指令来完成高分辨率计时。对于声音和视频处理来说,Windows多媒体计时器已经足够精确了,但是对于小于等于12毫秒的帧时间,它们无法提供时间增量(delta-time)信息。鉴于RDTSC的局限性, Windows API通过函数QueryPerformanceCounter和QueryPerformanceFrequency揭示了一种更正确的方法来实现这个功能。
         这种RDTSC风格的计时方式存在三个基本问题:
         使用RDTSC直接默认了线程运行在同一个处理器上。多处理器系统和双核系统不担保每个核间循环计数器的同步。当结合现代电源管理技术时,这个现象会加剧,因为现代电源管理技术会在不同的时间闲置(idle)或恢复(restore)不同的核,这通常会导致各个核失去同步。对于应用程序来说,这通常会引起干扰或潜在的崩溃,因为线程在不同的处理器之间跳动执行,其取得的时间值由于处理器之间的不同步而不同,这要么引起大增量、负增量,要么引起计时中断。
         RDTSC使得应用程序请求的计时信息完全依赖于处理器的循环计数器。很多年以来,这是得到高精度计时信息的最佳方法,但是现在新一代的主板贡献了一些计时设备,这些设备能提供高分辨率且没有RDTSC那些缺点的计时信息。
         以前总是假设在程序的生命期内,CPU的频率是固定的。然而,有了现代电源管理技术,这成了一个不正确的假设。起初这种技术只用在膝上、移动计算机上,但是现在它正积极地被用到很多高端台式PC上,而且禁止使用它通常是无法让用户接收的。
    建议
         游戏需要精确的计时信息,但是也需要以某种方式实现计时代码,这种方式必须避免与RDTSC使用相关的问题。在实现高分辨率计时之时,应该采取以下几步:
         用QueryPerformanceCounter 和QueryPerformanceFrequency 两个API函数来取代RDTSC 指令。 这些API或许会使用RDTSC,但也可能使用能提供高分辨率计时信息的主板计时芯片或其他一些系统服务来代替。但是,RDTSC比QueryPerformanceCounter快多了,因为后者是一个API调用,尽管如此,QueryPerformanceCounter还是可以在每帧中被调用几百次而没有明显地影响。在每一帧中,游戏应试着尽量少地调用QueryPerformanceCounter来避免性能损失。
         在计算增量的时候,增量值应被控制在确保计时误差不会引起崩溃或时间相关计算(time-related computation)的不稳定的程度。控制范围应从0(以防止产生负增量)到某个合理的基于最低期望帧率的值。注意:在调试应用程序的时候,这个控制可能不需要,但是如果在进行性能分析或运行游戏于某些非优化模式中时,必须使用它。
         因为QueryPerformanceCounter / QueryPerformanceFrequency两个API是支持多处理器的,所以线程从一个处理器移向另一个的时候,BIOS或主板驱动程序中的Bug可能会导致这些函数返回不同的值。我们建议所有的游戏计时都在单个线程上计算,并且用SetThreadAffinityMask这个Windows API来让该线程始终运行于单个处理器上。通常这个是游戏的主线程。所有其他线程应被设计成不收集和使用它们自己的计时器数据。我们不建议使用“工作线程(worker thread)”来计算计时,因为这会编程同步瓶颈。建议工作线程设计成从主线程读取时间戳。因为工作线程仅读取时间戳,所以没有必要使用临界区了。我们同样不建议让多线程来计算计时并将每个线程与一个特定的处理器关联,因为这将大大降低多核系统的吞吐量。
         系统运行时,QueryPerformanceFrequency这个API不会改变频率,所以它只需被调用一次。
    应用程序兼容性解决方案
         很多游戏开发人员使用对RDTSC的假设很多年了,因此当他们现存的应用程序在多处理器 / 多核系统上运行时,有计时实现方法的原因,很可能会出现问题。这通常表现为小故障或慢动作移动。缺少电源管理意识是没有简单的补救措施的,但是有一种现存的缓解方法,那就是强迫应用程序总是运行在单个处理器上,即使是在多处理器系统中。

    微软游戏技术组软件设计工程师 Chuck Walbourn

    Beware of QueryPerformanceCounter()

    When it comes to high-precision timing on Windows, many have gotten used to using the CPU's time stamp counter (TSC). The time stamp counter is a 64-bit counter that was added to most x86 CPUs starting around the Pentium era, and which counts up at the clock rate of the CPU. The TSC is generally readable via the RDTSC instruction from user mode, making it the fastest, easiest, and most precise time base available on modern machines.

    Alas, it is rather unsafe to use.

    The first problem you quickly run into is that there is no easy way to accurately and reliably determine the clock speed of the CPU, short of perhaps doing calibration over a longish period of time. Sometimes you don't need super accuracy or only need to deal with timing ratios, in which this doesn't matter. However, you're still screwed when you discover that on CPUs with speed switching, the speed at which the TSC counts will change when the CPU speeds up or slows down, which makes the TSC's rate swing all over the place. And if this weren't enough, the TSC is not always synchronized on dual-core or SMP systems, meaning that the reading from the TSC will jump back and forth by as much as 0.2ms as the kernel moves your thread back and forth across the CPUs. Programs which do not have adequate safety protection may be surprised when time momentarily runs backwards.

    For reasons like these, Microsoft now recommends that you use QueryPerformanceCounter() to do high-precision timing. What they don't tell you, though, is that QPC() is equally broken.

    The documentation for QueryPerformanceFrequency() says that not all systems have a high-performance counter. Truth be told, I've never seen a system that didn't support QPF/QPC, including ones running Windows 98, NT4, and XP. However, the timer that is used can vary widely between systems. On Win9x systems that I've seen, QPF() returns 1193181 -- which looks suspiciously like the clock rate of the venerable 8253/8254 timer. On a PIII-based Windows 2000 system, I got 3549545, which happens to be the frequency of the NTSC color subcarrier, but is probably just a factor of a common clock crystal used by some chipset timer. And I've also seen the CPU clock speed show up, or CPU clock divided by 3.

    Some of these timers used for QPC also have bugs.

    When I was looking at some anomalous capture logs from one of my systems, I noticed that the global_clock values from the capture subsystem, which were recorded in the capture log, occasionally jumped forward or backward by a few seconds compared to the video capture clock. (While video capture drivers are notoriously flaky, there were no gaps in the video and I'm pretty sure my PlayStation 2 didn't burp for three seconds.) When I tried Windows XP x64 Edition, the HAL used the CPU TSC for QueryPerformanceCounter() without realizing that Cool & Quiet would cause it to run at half normal speed. And recently, I've had the pleasure of seeing a dual-core system where use of the TSC exposed QPC-based programs to the same CPU-mismatch bug that RDTSC incurred. So, realistically, using QPC() actually exposes you to all of the existing problems of the time stamp counter AND some other bugs.

    So, what to do?

    I switched VirtualDub's capture subsystem from QueryPerformanceCounter() to timeGetTime(). I had to give up microsecond precision for only millisecond, but it's more reliable. If you don't really need high precision, you can use GetTickCount(), which has terrible precision on Win9x (55ms), but it's reliable, and it's fast, since it just reads a counter in memory. If you're a user suffering from this problem, you can try fixing the problem by adding /usepmtimer to the end of the boot.ini entry, which switches QPC() to use an alternate timer (usual disclaimers apply; back up data before trying; no purchase necessary; void where prohibited).

    April 25

    关闭 min max 宏

    #define  NOMINMAX 去掉 windows.h 头文件中的min max宏.
    这样才能使用标准库 limits 中的 min max 函数

    April 02

    驱动级模拟(硬件模拟)

    转个文章,即使使用directinput 也不能屏蔽按键精灵的输入,而按键精灵不过调用了第三方的dll.来实现的知道这些有利于检测简单的按键程序
    对于检测加速器就更简单了,其实只要你调试一下程序就能发现所谓加速器的秘密。不过是让2次timeGetTime 之类函数差值变长。甚好解决

      如果上面的方法你都试过了,可是你发现目标程序却仍然顽固的不接受你模拟的消息,寒~~~~~~~~~还好,我还剩下最后一招,这就是驱动级模拟:直接读写键盘的硬件端口!

      有一些使用DirectX接口的游戏程序,它们在读取键盘操作时绕过了windows的消息机制,而使用DirectInput.这是因为有些游戏对实时性控制的要求比较高,比如赛车游戏,要求以最快速度响应键盘输入。而windows消息由于是队列形式的,消息在传递时会有不少延迟,有时1秒钟也就传递十几条消息,这个速度达不到游戏的要求。而DirectInput则绕过了windows消息,直接与键盘驱动程序打交道,效率当然提高了不少。因此也就造成,对这样的程序无论用PostMessage或者是keybd_event都不会有反应,因为这些函数都在较高层。对于这样的程序,只好用直接读写键盘端口的方法来模拟硬件事件了。要用这个方法来模拟键盘,需要先了解一下键盘编程的相关知识。

      在DOS时代,当用户按下或者放开一个键时,就会产生一个键盘中断(如果键盘中断是允许的),这样程序会跳转到BIOS中的键盘中断处理程序去执行。打开windows的设备管理器,可以查看到键盘控制器由两个端口控制。其中&H60是数据端口,可以读出键盘数据,而&H64是控制端口,用来发出控制信号。也就是,从&H60号端口可以读此键盘的按键信息,当从这个端口读取一个字节,该字节的低7位就是按键的扫描码,而高1位则表示是按下键还是释放键。当按下键时,最高位为0,称为通码,当释放键时,最高位为1,称为断码。既然从这个端口读数据可以获得按键信息,那么向这个端口写入数据就可以模拟按键了!用过QbASIC4.5的朋友可能知道,QB中有个OUT命令可以向指定端口写入数据,而INP函数可以读取指定端口的数据。那我们先看看如果用QB该怎么写代码:

    假如你想模拟按下一个键,这个键的扫描码为&H50,那就这样
    OUT &H64,&HD2   '把数据&HD2发送到&H64端口。这是一个KBC指令,表示将要向键盘写入数据
    OUT &H60,&H50   '把扫描码&H50发送到&H60端口,表示模拟按下扫描码为&H50的这个键
    那么要释放这个键呢?像这样,发送该键的断码:
    OUT &H64,&HD2   '把数据&HD2发送到&H64端口。这是一个KBC指令,表示将要向键盘写入数据
    OUT &H60,(&H50 or &H80)   '把扫描码&H50与数据&H80进行或运算,可以把它的高位置1,得到断码,表示释放这个键

      好了,现在的问题就是在VB中如何向端口写入数据了。因为在windows中,普通应用程序是无权操作端口的,于是我们就需要一个驱动程序来帮助我们实现。在这里我们可以使用一个组件WINIO来完成读写端口操作。什么是WINIO?WINIO是一个全免费的、无需注册的、含源程序的WINDOWS2000端口操作驱动程序组件(可以到http://www.internals.com/上去下载)。它不仅可以操作端口,还可以操作内存;不仅能在VB下用,还可以在DELPHI、VC等其它环境下使用,性能特别优异。下载该组件,解压缩后可以看到几个文件夹,其中Release文件夹下的3个文件就是我们需要的,这3个文件是WinIo.sys(用于win xp下的驱动程序),WINIO.VXD(用于win 98下的驱动程序),WinIo.dll(封装函数的动态链接库),我们只需要调用WinIo.dll中的函数,然后WinIo.dll就会安装并调用驱动程序来完成相应的功能。值得一提的是这个组件完全是绿色的,无需安装,你只需要把这3个文件复制到与你的程序相同的文件夹下就可以使用了。用法很简单,先用里面的InitializeWinIo函数安装驱动程序,然后就可以用GetPortVal来读取端口或者用SetPortVal来写入端口了。好,让我们来做一个驱动级的键盘模拟吧。先把winio的3个文件拷贝到你的程序的文件夹下,然后在VB中新建一个工程,添加一个模块,在模块中加入下面的winio函数声明:

    Declare Function MapPhysToLin Lib "WinIo.dll" (ByVal PhysAddr As Long, ByVal PhysSize As Long, ByRef PhysMemHandle) As Long
    Declare Function UnmapPhysicalMemory Lib "WinIo.dll" (ByVal PhysMemHandle, ByVal LinAddr) As Boolean
    Declare Function GetPhysLong Lib "WinIo.dll" (ByVal PhysAddr As Long, ByRef PhysVal As Long) As Boolean
    Declare Function SetPhysLong Lib "WinIo.dll" (ByVal PhysAddr As Long, ByVal PhysVal As Long) As Boolean
    Declare Function GetPortVal Lib "WinIo.dll" (ByVal PortAddr As Integer, ByRef PortVal As Long, ByVal bSize As Byte) As Boolean
    Declare Function SetPortVal Lib "WinIo.dll" (ByVal PortAddr As Integer, ByVal PortVal As Long, ByVal bSize As Byte) As Boolean
    Declare Function InitializeWinIo Lib "WinIo.dll" () As Boolean
    Declare Function ShutdownWinIo Lib "WinIo.dll" () As Boolean
    Declare Function InstallWinIoDriver Lib "WinIo.dll" (ByVal DriverPath As String, ByVal Mode As Integer) As Boolean
    Declare Function RemoveWinIoDriver Lib "WinIo.dll" () As Boolean

    ' ------------------------------------以上是WINIO函数声明-------------------------------------------

    Declare Function MapVirtualKey Lib "user32" Alias "MapVirtualKeyA" (ByVal wCode As Long, ByVal wMapType As Long) As Long

    '-----------------------------------以上是WIN32 API函数声明-----------------------------------------

    再添加下面这个过程:
    Sub KBCWait4IBE()   '等待键盘缓冲区为空
    Dim dwVal As Long
      Do
      GetPortVal &H64, dwVal, 1
    '这句表示从&H64端口读取一个字节并把读出的数据放到变量dwVal中
    'GetPortVal函数的用法是GetPortVal 端口号,存放读出数据的变量,读入的长度
      Loop While (dwVal And &H2)
    End Sub
    上面的是一个根据KBC规范写的过程,它的作用是在向键盘端口写入数据前等待一段时间,后面将会用到。
    然后再添加如下过程,这2个过程用来模拟按键:

    Public Const KBC_KEY_CMD = &H64    '键盘命令端口
    Public Const KBC_KEY_DATA = &H60   '键盘数据端口

    Sub MyKeyDown(ByVal vKeyCoad As Long)  
    '这个用来模拟按下键,参数vKeyCoad传入按键的虚拟码
    Dim btScancode As Long
    btScancode = MapVirtualKey(vKeyCoad, 0)
      
        KBCWait4IBE   '发送数据前应该先等待键盘缓冲区为空
        SetPortVal KBC_KEY_CMD, &HD2, 1     '发送键盘写入命令
    'SetPortVal函数用于向端口写入数据,它的用法是SetPortVal 端口号,欲写入的数据,写入数据的长度
        KBCWait4IBE
        SetPortVal KBC_KEY_DATA, btScancode, 1  '写入按键信息,按下键
        
    End Sub

    Sub MyKeyUp(ByVal vKeyCoad As Long)  
    '这个用来模拟释放键,参数vKeyCoad传入按键的虚拟码
    Dim btScancode As Long
    btScancode = MapVirtualKey(vKeyCoad, 0)

       KBCWait4IBE    '等待键盘缓冲区为空
       SetPortVal KBC_KEY_CMD, &HD2, 1 '发送键盘写入命令
       KBCWait4IBE
       SetPortVal KBC_KEY_DATA, (btScancode or &H80), 1 '写入按键信息,释放键

    End Sub


      定义了上面的过程后,就可以用它来模拟键盘输入了。在窗体模块中添加一个定时器控件,然后加入以下代码:

    Private Sub Form_Load()

    If InitializeWinIo = False Then  
    '用InitializeWinIo函数加载驱动程序,如果成功会返回true,否则返回false
       MsgBox "驱动程序加载失败!"
       Unload Me
    End If
    Timer1.Interval=3000
    Timer1.Enabled=True
    End Sub

    Private Sub Form_Unload(Cancel As Integer)
    ShutdownWinIo '程序结束时记得用ShutdownWinIo函数卸载驱动程序
    End Sub

    Private Sub Timer1_Timer()
    Dim VK_A as Long = &H41
    MyKeyDown VK_A  
    MyKeyUp VK_A    '模拟按下并释放A键
    End Sub

      运行上面的程序,就会每隔3秒钟模拟按下一次A键,试试看,怎么样,是不是对所有程序都有效果了?

    需要注意的问题:

      要在VB的调试模式下使用WINIO,需要把那3个文件拷贝到VB的安装目录中。
      键盘上有些键属于扩展键(比如键盘上的方向键就是扩展键),对于扩展键不应该用上面的MyKeyDown和MyKeyUp过程来模拟,可以使用下面的2个过程来准确模拟扩展键:

    QUOTE:
    Sub MyKeyDownEx(ByVal vKeyCoad As Long)    '模拟扩展键按下,参数vKeyCoad是扩展键的虚拟码
    Dim btScancode As Long
    btScancode = MapVirtualKey(vKeyCoad, 0)

       KBCWait4IBE    '等待键盘缓冲区为空
       SetPortVal KBC_KEY_CMD, &HD2, 1    '发送键盘写入命令
       KBCWait4IBE
       SetPortVal KBC_KEY_DATA, &HE0, 1 '写入扩展键标志信息

       KBCWait4IBE    '等待键盘缓冲区为空
       SetPortVal KBC_KEY_CMD, &HD2, 1    '发送键盘写入命令
       KBCWait4IBE
       SetPortVal KBC_KEY_DATA, btScancode, 1 '写入按键信息,按下键
    End Sub

    Sub MyKeyUpEx(ByVal vKeyCoad As Long)    '模拟扩展键弹起
    Dim btScancode As Long
    btScancode = MapVirtualKey(vKeyCoad, 0)

       KBCWait4IBE    '等待键盘缓冲区为空
       SetPortVal KBC_KEY_CMD, &HD2, 1    '发送键盘写入命令
       KBCWait4IBE
       SetPortVal KBC_KEY_DATA, &HE0, 1 '写入扩展键标志信息
      
       KBCWait4IBE    '等待键盘缓冲区为空
       SetPortVal KBC_KEY_CMD, &HD2, 1    '发送键盘写入命令
       KBCWait4IBE
       SetPortVal KBC_KEY_DATA, (btScancode or &H80), 1 '写入按键信息,释放键
      
    End Sub

      还应该注意的是,如果要从扩展键转换到普通键,那么普通键的KeyDown事件应该发送两次。也就是说,如果我想模拟先按下一个扩展键,再按下一个普通键,那么就应该向端口发送两次该普通键被按下的信息。比如,我想模拟先按下左方向键,再按下空格键这个事件,由于左方向键是扩展键,空格键是普通键,那么流程就应该是这样的:

    MyKeyDownEx VK_LEFT    '按下左方向键
    Sleep 200          '延时200毫秒
    MyKeyUpEx VK_LEFT    '释放左方向键

    Sleep 500
    MyKeyDown VK_SPACE    '按下空格键,注意要发送两次
    MyKeyDown VK_SPACE
    Sleep 200
    MyKeyUp VK_SPACE    '释放空格键
    March 31

    PeekMessage 与 GetMessage

    PeekMessage 似乎是针对一个窗口线程的而GetMessage 是对整个进程的。当一个进程创建1个以上窗口的(这个很重要),当切换进程中不同窗口的时候PeekMessage 会得到WM_QUIT消息。而GetMessage不会. 似乎GetMessage比PeekMessage能更快的相应消息,但没有消息时GetMessage会挂起进程等待消息,所以好的做法是联合使用2个函数:

    while (PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE))        //是否有消息
    {
        if ( !GetMessage (&msg, NULL, 0, 0) )               
        {
           //WM_QUIT 消息处理
        }

        TranslateMessage (&msg);
        DispatchMessage (&msg);
    }

    March 21

    sdk 中使用 richedit2

    1 使用richedit 控件首先要加载 richedit 的dll.
    LoadLibrary(_T("riched20.dll"));
    2 加入对 richedit.h 头文件的引用后,创建窗口
    hwndBuffer    =   CreateWindow(RICHEDIT_CLASS,NULL,WS_CHILD|WS_VISIBLE|ES_MULTILINE|WS_VSCROLL|ES_SUNKEN
                                |ES_AUTOVSCROLL|ES_READONLY|ES_SAVESEL,6,30,526,354,hWnd,(HMENU)EDIT_ID,hInstance,NULL); 
    这里创建了一个只读的窗口,这样只能通过函数输入文字。
    3 设置颜色:
    //设置背景色
    SendMessage( hwndBuffer, EM_SETBKGNDCOLOR, 0, RGB(0,0,0) ); 
    //设置文字颜色
    CHARFORMAT2 charFmt;
    ZeroMemory(&charFmt,sizeof(charFmt));
    charFmt.cbSize = sizeof(charFmt);
    charFmt.dwMask = CFM_COLOR;      //只修改文字颜色标志
    charFmt.crTextColor = RGB(0,179,0);//设置字体颜色  
    SendMessage(hwndBuffer,EM_SETCHARFORMAT,0,(LPARAM)&charFmt);
    //修改部分文字颜色
    SendMessage(hwndBuffer,EM_SETSEL,0,10);    //选中0-10的文字
    charFmt.crTextColor = it->clr;
    //修改文字颜色
    SendMessage(hwndBuffer,EM_SETCHARFORMAT,SCF_SELECTION,(LPARAM)&charFmt);           
    注意对于richedit2.0  汉字仅作为一个字符。对于unicode 工程无所谓。但对于多字节来说一个汉字为2个字符。所以你通过strlen之类计算的字符串长度会比richedit 认为的要长如:
    "大家好"。计算为6长度。但对于richedit来说为3个.发送EM_SEL消息,设置选中消息时必须以richedit为准.
    SendMessage(hwndBuffer,EM_SETSEL,0,3);    如果指定为6将超出范围
    4 滚动滚动条:
    SendMessage(hwndBuffer,WM_VSCROLL, SB_BOTTOM, 0);    //滚动到文档的最后面
    5 游标
    int temp1,temp2;  //获取起始和结束位置
    SendMessage(hwndBuffer,EM_GETSEL,(WPARAM)&temp1,(LPARAM)&temp2);
    //在文档末尾输入字符:
    SendMessage(hwndBuffer, EM_SETSEL,-1,-1);
    SendMessage(hwndBuffer, EM_REPLACESEL, TRUE, (LPARAM) buffer );

    March 12

    WS_OVERLAPPED|WS_BORDER|WS_CAPTION|WS_VISIBLE|WS_SYSMENU bug ?

    无语了,当使用这个状态创建窗口后,移动窗口,窗口向左移动到windows 外,图形居然会自动和windows窗口对其。(当创建窗口分辨率大于等于windows分辨率的时候,郁闷的是小于当前桌面分辨率也没有这种情况)

    windowstylebug

    改为:WS_POPUP|WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_VISIBLE 后恢复正常
    windowstylebug1

    January 14

    双核原子操作

    #include "stdafx.h"
    #include <windows.h>
    #include <process.h>

    struct Tf
    {
     unsigned int tid;
     unsigned int a;
     unsigned int b;
     __int64 time;
    };
    Tf thread[400];

    inline __int64 GetCycleCount()
    {
     __asm
     {
      rdtsc
     }
    }

    static unsigned int __stdcall QueueThreadFunc(void * pv)
    {
     unsigned int tid = GetCurrentThreadId();
     Tf* thread = (Tf *) pv;
     thread->tid = tid;
     static int a = 10;
     bool f;
     int b;
     while(1)
     {
      b = a;
      int c = b+1;
      thread->b = b;
      _asm
      {
       lea ecx,a
       mov eax,b
       mov ebx,c
       lock cmpxchg [ecx],ebx
       mov eax,dword ptr [ecx]      //对于双核cpu 马上读回锁定的数据,并不能保证(对于单核到是可以)原子操作。
       mov dword ptr c,eax
       setz f
      }
      if(f)
      {
       thread->a = c;
       thread->time = GetCycleCount();
       break;
      }
     }
     

    }

    int _tmain(int argc, _TCHAR* argv[])
    {
     HANDLE ThreadData[400];
     __int64 base = GetCycleCount();
     for(int i = 0; i < 400; ++i)
     {
      ThreadData[i]= (HANDLE)_beginthreadex(NULL, 0, QueueThreadFunc,&thread[i],CREATE_SUSPENDED,0);
     }

     for(int i = 0; i < 400; ++i)
     {
      ResumeThread(ThreadData[i]);   //这样并发性更强
     }

     Sleep(100);

     for(int i=0;i<400;i++)
     {
      printf("tid :%d, a:%d b:%d time:%d \n",thread[i].tid,thread[i].a,thread[i].b,thread[i].time - base); 
      if(thread[i].a != thread[i].b + 1)
      {
       printf("not equal \n");
      }
     }
     return 0;
    }

    January 05

    LoadLibrary 998

    郁闷的问题。LoadLibrary 后 FreeLibrary 在load 任何dll 都会失败。用GetLastError 获得的错误码是998.浪费了2-3个小时,最后调试看卸载过程发现了 vld .在dll中引入了vld的头文件检查内存泄漏。但 FreeLibrary 并没有把它卸载掉。或许因为主进程中也用了vld.导致FreeLibrary dll 不能释放它(vld 加载释放由vld自己决定的)。当再次LoadLibrary 时因为资源没有释放完全,就会发生访问冲突
    调试菜单->异常->win32 exceptions ->C00000005 Acess violation 修改为中断。可以在调试时中断访问违规错误。尽管杯水车薪,还算学到了一手。

    December 29

    无锁多线程纹理资源读取

    崩溃了一天,终于搞定了,惨不忍睹啊。
    好处了懒得说了。
    还好zlib 是线程安全的。一直没注意,要编译了个多线程版本的才行。
    挂了几次发现工程设成了单线程,居然在这种情况把不使用压缩包的多线程读取文件调试过了。汗
    又忘掉了vc 模板类必须全部写道头文件中。编译通过链接不过,莫名其妙了好久,反映迟钝。郁闷以后还是少用模板好了。甚至于标准库。
    很多内部情况,挂了也莫名其妙。本来想用其 map .后来当取得一个成员迭代器时。保存之,是否指向的元素会发生变化。没几个人能确定。唉,还是自己搞个简单版的。虽然是造轮子。但更适合我:当取得资源后获得资源句柄h,用h 我可以一次跳转到资源位置(相当于数组缩影)。亦可以直接跳到 hash map的元素 hash 后的位置。不用再去求 hash 值了。从 hash 表中删除也很easy。直接把资源对象从链表中去掉就好。有时积木并不能拼出自己想要的东西。都锯下来一点性能,结构都好很多
    首先通过给定文件名称 hash . 记录hash key 。给出句柄handle。多线程去加载纹理。当画纹理是可以通过判断 manager[handle] 中 LPDIRECT3DTEXTURE9 成员是否为初始值 NULL 即可。不为 NULL 。表示加载成功。
    有时间再搞个缩减的版本丢上来。

    线程函数性能评测:

            LARGE_INTEGER n0,n1;
            QueryPerformanceCounter(&n0);
            __asm RDTSC               //同步cpu指令.避免因为乱序执行造成即时错误
            for(int i = 0; i < 2; ++i)
            {
                m_aThreadData[i].m_pOwer = this;
                unsigned int tid;
                m_aThreadData[i].m_hThread = (HANDLE)_beginthreadex(NULL, 0, QueueThreadFunc, &m_aThreadData[i], 0, &tid);
            }
            __asm RDTSC
            QueryPerformanceCounter(&n1);
            ri->Printf(PRINT_ALL,_T("CreateThread 耗时: %d\n"),n1.QuadPart - n0.QuadPart);       
    __asm RDTSC
            QueryPerformanceCounter(&n0);
            for(int i = 0; i < 2; ++i)
            {
                SuspendThread(m_aThreadData[i].m_hThread);
            }
            QueryPerformanceCounter(&n1);
    __asm RDTSC
            ri->Printf(PRINT_ALL,_T("SuspendThread 耗时: %d\n"),n1.QuadPart - n0.QuadPart);       

    __asm RDTSC
            QueryPerformanceCounter(&n0);
            for(int i = 0; i < 2; ++i)
            {
                ResumeThread(m_aThreadData[i].m_hThread);
            }
            QueryPerformanceCounter(&n1);
    __asm RDTSC
            ri->Printf(PRINT_ALL,_T("ResumeThread 耗时: %d\n"),n1.QuadPart - n0.QuadPart);

    p4 2.8G 非双核cpu. 大概 beginthreadex: 420 SuspendThread:25 ResumeThread:20.
    看来使用线程池比频繁创建还是能节省很多时间的.
    所谓无锁对于单核 cpu 可以用double-check.即设置一个状态变量依靠判断其状态来决定是否访问数据
    比如读取一个buffer。一个变量表示是否写入完毕
    if(ready)
    {
       if(ready)
       {
           //读取
       }
    }

    但对于多核就不适合了。不过intel很早就有一个硬件指令lock 用来锁定总线
         int oldval = 1;
         int newval = 0;
         bool f;
        _asm
        {
          mov ecx,_ptr
          mov eax,oldVal
          mov ebx,newVal
          lock cmpxchg [ecx],ebx
          setz f
       }
    // 判断*_ptr值是否等于 oldVal。相等则把newVal写入*_ptr 并返回 true
    // 否则返回 false,不改变任何值

    November 22

    软件Release 版本 Crash 堆栈信息收集

    比较详细的地址是:http://www.codeproject.com/debug/XCrashReportPt3.asp

    不过作者考虑了没有vc等情况。
    其实如果有vc使用ms 的 MiniDumpWriteDump 将会非常简单. 如下面100行左右搞定,理解、转换为unicode 编码都简单很多.只需要装入相应的头文件即可记录crash dump 文件。用vc打开dump 文件。按F5即可运行到crash地点
    vc Release 工程必须设置:
    链接器 ->调试-> 生成调试信息 是(/DEBUG)
    链接器->优化->引用->消除未引用数据(/OPT:REF)

    头文件 minidump.h

    #pragma once
    #include <windows.h>
    #include <tchar.h>
    #if _MSC_VER < 1300
    #define DECLSPEC_DEPRECATED
    // VC6: change this path to your Platform SDK headers
    #include "c:\\dev7\\vs\\devtools\\common\\win32sdk\\include\\dbghelp.h"            // must be XP version of file
    #else
    // VC7: ships with updated headers
    #include "dbghelp.h"
    #endif

    // based on dbghelp.h

    class MiniDumper
    {
        typedef BOOL (WINAPI* MINIDUMPWRITEDUMP)(HANDLE hProcess, DWORD dwPid, HANDLE hFile, MINIDUMP_TYPE DumpType,
                                                CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
                                                CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
                                                CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam);
    private:
        static LPCTSTR m_szAppName;
        static LONG WINAPI TopLevelFilter(struct _EXCEPTION_POINTERS *pExceptionInfo);
    public:
        MiniDumper(LPCTSTR szAppName);
        ~MiniDumper();
    };


    //源文件 minidump.cpp

    #include "minidump.h"
    #include <assert.h>
    #include <stdio.h>
    #include <direct.h>

    LPCTSTR MiniDumper::m_szAppName;
    #ifndef _DEBUG
    MiniDumper dumper(_T("xyj2007"));
    #endif

    MiniDumper::MiniDumper(LPCTSTR szAppName)
    {
        // if this assert fires then you have two instances of MiniDumper which is not allowed
        assert( m_szAppName==NULL );            //确认只创建一个 minidumper
        m_szAppName =_tcsdup(szAppName);
        ::SetUnhandledExceptionFilter(TopLevelFilter);
    }

    MiniDumper::~MiniDumper()
    {
        if(m_szAppName)
        {
            free( (void *) m_szAppName);
            m_szAppName = NULL;
        }
    }

    LONG MiniDumper::TopLevelFilter( struct _EXCEPTION_POINTERS *pExceptionInfo )
    {
        LONG retval = EXCEPTION_CONTINUE_SEARCH;
        HWND hParent = NULL;                        // find a better value for your app

        // firstly see if dbghelp.dll is around and has the function we need
        // look next to the EXE first, as the one in System32 might be old
        // (e.g. Windows 2000)
        HMODULE hDll = NULL;
        TCHAR szDbgHelpPath[_MAX_PATH];

        if (GetModuleFileName( NULL, szDbgHelpPath, _MAX_PATH ))
        {
            TCHAR *pSlash = _tcsrchr( szDbgHelpPath, _T('\\') );
            if (pSlash)
            {
                _tcscpy(pSlash+1, _T("DBGHELP.DLL"));
                hDll = ::LoadLibrary( szDbgHelpPath );
            }
        }

        if (hDll==NULL)
        {
            // load any version we can
            hDll = ::LoadLibrary(_T("DBGHELP.DLL"));
        }

        LPCTSTR szResult = NULL;

        if (hDll)
        {
            MINIDUMPWRITEDUMP pDump = (MINIDUMPWRITEDUMP)::GetProcAddress(hDll,"MiniDumpWriteDump");
            if (pDump)
            {
                TCHAR szDumpPath[_MAX_PATH];
                TCHAR szScratch [_MAX_PATH];

                // work out a good place for the dump file
                _tgetcwd(szDumpPath,_MAX_PATH);
                _tcscat( szDumpPath, _T("\\"));

                _tcscat( szDumpPath, m_szAppName );
                _tcscat( szDumpPath, _T(".dmp"));

                // ask the user if they want to save a dump file
                if (::MessageBox(NULL,_T("程序发生意外,是否保存一个文件用于诊断?"),m_szAppName,MB_YESNO)==IDYES)
                {
                    // create the file
                    HANDLE hFile = ::CreateFile( szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS,
                                                FILE_ATTRIBUTE_NORMAL, NULL );

                    if (hFile!=INVALID_HANDLE_VALUE)
                    {
                        _MINIDUMP_EXCEPTION_INFORMATION ExInfo;
                        ExInfo.ThreadId = ::GetCurrentThreadId();
                        ExInfo.ExceptionPointers = pExceptionInfo;
                        ExInfo.ClientPointers = NULL;

                        // write the dump
                        BOOL bOK = pDump( GetCurrentProcess(),GetCurrentProcessId(),hFile,MiniDumpNormal,&ExInfo,NULL,NULL);
                        if (bOK)
                        {
                            _stprintf( szScratch, _T("保存文件到:'%s'"), szDumpPath );
                            szResult = szScratch;
                            retval = EXCEPTION_EXECUTE_HANDLER;
                        }
                        else
                        {
                            _stprintf( szScratch, _T("保存文件到 '%s'失败,(错误号: %d)"), szDumpPath, GetLastError() );
                            szResult = szScratch;
                        }
                        ::CloseHandle(hFile);
                    }
                    else
                    {
                        _stprintf( szScratch, _T("在'%s'创建 dump 文件失败,(错误号 %d)"), szDumpPath, GetLastError() );
                        szResult = szScratch;
                    }
                }
            }
            else
            {
                szResult = _T("dbghelp.dll 文件太旧,不能支持MiniDumpWriteDump函数");
            }
        }
        else
        {
            szResult = _T("dbghelp.dll 文件不存在");
        }

        if (szResult)
        {
            ::MessageBox( NULL, szResult, m_szAppName, MB_OK );
        }

        return retval;
    }

    September 05

    给 CListCtrl 加背景图片和文字居中

    .h
    CListCtrl m_CServerList

    .cpp

    memset(&lvc,0,sizeof(lvc));
    lvc.mask =  LVCF_TEXT | LVCF_WIDTH | LVCF_FMT;  
    lvc.pszText = _T("Test1");  
    lvc.cx = 96;  
    lvc.fmt = LVCFMT_CENTER;                                     //注意第一行是不能居中的.可以在第二行插入相同内容,
    m_CServerList.InsertColumn(0,&lvc);                      //然后删除第一行

    lvc.mask =  LVCF_TEXT | LVCF_WIDTH | LVCF_FMT;   
    lvc.pszText = _T("Test2");  
    lvc.cx = CtrlBkImageRt.Width() - 96;  
    lvc.fmt=   LVCFMT_CENTER;  
    m_CServerList.InsertColumn(1,&lvc);  


    char path[MAX_PATH];
    _getcwd(path,200);                                              //获得当前进程目录位置,需要包含<direct.h> 头文件
    strcat(path,"\\res\\serback.jpg");
    if(FileExists(path))
    {
        m_CServerList.SetTextBkColor(CLR_NONE);            //去掉文字背景色
        m_CServerList.SetBkImage(TEXT(path),TRUE);       //设置背景图片 SetBkImage 函数不能使用相对目录
        m_CServerList.SetTextColor(RGB(0xFF,0xFF,0xFF)); //设置文字颜色为白色
    }


    bool FileExists(char* file)
    {
        bool flag = false;
        FILE* f = fopen(file,"r");
        if(f)
        {
            flag = true;
            fclose(f);
        }
        return flag;
    }

    这里唯一需要注意的是 SetBkImage 函数不能使用相对目录.还有初始化Com 特别是在线程中的话还要在线程中初始化Com环境

    August 02

    去掉webbrowse控件的滚动条

     // TODO: 在此处添加消息处理程序代码
    对于delphi vb 类很简单:
    IHTMLBodyElementDisp(IHTMLDocument2(WebBrowser1.document).body).scroll:= 'no';
     
    vc 要查询接口才能使用,相应代码如下:
     HRESULT hr = S_OK;
     IDispatch* pDocDisp = m_wndWeb.get_Document();   //m_wndWeb 为 webbrowse控件类对象
     if (pDocDisp)
     {
      IHTMLDocument2* pDoc = NULL;
      IHTMLElement* pElement = NULL;
      IHTMLBodyElement* pBodyElement = NULL;

      hr = pDocDisp->QueryInterface(IID_IHTMLDocument2, reinterpret_cast<void **>(&pDoc));
      hr = pDoc->get_body(&pElement);
      hr = pElement->QueryInterface(IID_IHTMLBodyElement, (void**)&pBodyElement);
      pBodyElement->put_scroll(::SysAllocString(L"no"));

      pBodyElement->Release();
      pElement->Release();
      pDoc->Release();
      pDocDisp->Release();
     } 

    网页中也必须有去掉滚动条的格式说明,可以参考baidu,google等网站
    July 25

    0x7c921230 处未处理的异常: 用户断点

      幸好这两天代码不多,当调试进程结束时遇到了这个错误,中断到了ntdll内,没有办法跟踪堆栈。 而非调试状态运行程序反而没有这个错误。当排出掉自己写的 int 3 之后(程序中也可以自己写断点中断)。感觉是退出的时候有些问题,在网上查找了一下,有人是DestroyWindow时窗口句柄不存在了。看了一下自己相关的代码没什么问题。无意间想到了在接受WM_CLOSE 消息时,调用了PostQuitMessage(0),case 块后面写成了 break ,进而调用了默认的消息处理进程。把它修改为return 0 之后,问题解决了。不知道是不是还有其他人遇到其它的情况,欢迎拍砖探讨

    检测屏幕分辨率和色深

     HDC hDC;
     int colorBits, xRes, yRes;
     hDC = GetDC( GetDesktopWindow() );    //获得当前桌面进程DC
     colorBits = GetDeviceCaps( hDC, BITSPIXEL ); //获得色深,如32位真彩色返回32
     xRes = GetDeviceCaps( hDC, HORZRES );  //获得分辨率宽度
     yRes = GetDeviceCaps( hDC, VERTRES );  //获得分辨率高度
     ReleaseDC( GetDesktopWindow(), hDC );  //释放DC

    修改分辨率:
    在VC中提供了修改显示设备(如显示器、打印机等等,本文只就显示器而言)属性的函数:ChangeDisplaySettings,该函数能够按照你的需要对显示设备作出相应的修改。其函数申明如下:
      LONG ChangeDisplaySettings(LPDEVMODE lpDevMode, DWORD dwflags);
      其参数的含义如下:
      lpDevMode:一个指向DEVMODE数据结构的指针,DEVMODE的数据结构描述了欲设定显示器的各类属性值。通常情况下使用到的参数有:
      dmSize:所用DEVMODE数据结构的大小(以Bytes为单位)
      dmBitsPerPel :每象素所使用的显存位数(Bits)
      dmPelsWidth Pixel width :水平分辨率(点数)
      dmPelsHeight Pixel height :垂直分辨率(点数)
      dmDisplayFrequency Mode frequency :显示刷新率,以赫兹为单位
      dmFields:通常情况下,不同的显示设备(如打印机)用到的DEVMODE数据结构的内容不同,比如设定打印机时,你不会用到dmDisplayFrequency属性。所以,在你使用DEVMODE数据结构时,应向系统说明你具体用到的有效数据成员,dmFields的用处便在于此。如果在程序中只用到dmPelsWidth(水平分辨率)和dmPelsHeight(垂直分辨率),那么该值应为DM_PELSWIDTH|DM_PELSHEIGHT。DM_DISPLAYFREQUENCY 为刷新频率 DM_BITSPERPEL 表示色深
      Dwflags:表明对显示设备的修改方式。具体取值有以下几种:
      0 :动态改变显示设备属性
      CDS_UPDATEREGISTRY:动态改变显示设备属性并修改注册表相关设置,下次启动计算机时,本次所做的修改依然有效
      CDS_TEST: 测试所做的修改是否有效
         CDS_FULLSCREEN:
      该函数的返回值:
      DISP_CHANGE_SUCCESSFUL:修改成功
      DISP_CHANGE_RESTART :修改后需重新启动(在显示器设定中选择了“应用新的颜色前重新启动计算机”)
      DISP_CHANGE_FAILED :修改失败
      DISP_CHANGE_BADMODE:修改模式错误(比如你的显示器是单色的,但你却将之修改为256色的)

      当If lpDevMode为NULL且dwflags 为0时, 显示设备使用注册表当前值。ChangeDisplaySettings(NULL,0)

     
    July 12

    紫光输入法 冲突

      下面是一段常用的代码,而当使用 ctrl+shift 切换紫光输入法,输入文字。反复切换两次之后。GetMessage 系统函数取消息时冲突。而使用其它输入法都没有问题。经过 google 发现 firefox 也和紫光冲突,也有人和操作系统冲突的。说明此输入法 hook msg 有问题,不得不说紫光输入法确实好用。但下面的代码几乎每个mfc程序都差不多到了,或者有些程序直接调用GetMessage,而没有使用PeekMessage。冲突环境:使用 winxp+sp2 更新了所有系统补丁。紫光拼音3.0 。
      如果你是开发人员,还是建议你不要用这个输入法了。
    while (PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE))  //获得 WM_QUIT 消息
     {
      MSG   msg;
      if ( !GetMessage (&msg, NULL, 0, 0) )    
      {
      }
      // save the msg time, because wndprocs don't have access to the timestamp
      TranslateMessage (&msg);
      DispatchMessage (&msg);
     }
    June 16

    使用 IPicture 和 MemDC

      前一段时间,用MFC作个界面程序:画图并在上面写一段文字。原来用IPicture 在DC上 Render ,然后用DC TextOut 出文字,结果文字闪耀。在 OnEraseBkgnd(CDC* pDC) 用了 Mem dc 也没有起作用, 初始还以为刷新快不能做到不闪耀,后来才发现 mem dc 忘了设置 Bitmap ,没有 bitmap 对于内存dc不能画上内容,而 OnPaint 中又没有使用内存DC。导致一直是OnPaint 中DC在画图和写字。
    bool DrawPicture(CDC* pdc, IPicture* pic, const CRect& rcBounds)
    {
     CDC MemDC;  //定义内存DC
     MemDC.CreateCompatibleDC(NULL);   //注意不要写成.CreateCompatibleDC(pdc);
     MemDC.SetBkMode(TRANSPARENT);  //设置透明,使写字没有白色的背景块
     //得到图片的宽度和高度
     SIZE sizeInHiMetric;
     pic->get_Width(&sizeInHiMetric.cx);  //获取加载进pic的图像宽度和高度
     pic->get_Height(&sizeInHiMetric.cy);
     CBitmap bmpFace;   //创建bitmap
     bmpFace.CreateCompatibleBitmap(pdc, rcBounds.Width(), rcBounds.Height()); 
     //将这幅图片选入内存DC,原来memdc不起作用就是因为,bmp图片没有选入到dc中.郁闷
     CBitmap* pOldBmp = MemDC.SelectObject(&bmpFace); 
     //把整个图片 {0,cy,cx,-cy} .拷贝到 memdc 的rcBounds 区域 
     pic->Render(MemDC.GetSafeHdc(), rcBounds.left, rcBounds.top, rcBounds.right, rcBounds.bottom,
      0, sizeInHiMetric.cy, sizeInHiMetric.cx, -sizeInHiMetric.cy, &rcBounds);
     MemDC.SetTextColor(RGB(0,255,0));
     MemDC.TextOut(3,3,m_strFileTips); 
     //拷贝memdc (0,0,rcbounds.width,rcbounds.height) 区域到 pdc
     pdc->BitBlt(0,0,rcBounds.Width(),rcBounds.Height(),&MemDC,0,0,SRCCOPY);   //拷贝 memdc - > dc
     MemDC.SelectObject(pOldBmp);
     bmpFace.DeleteObject();
     MemDC.DeleteDC();
     return true;
    }
     
    对于镂空图可以使用
    void DrawTransPic(IPicture* pic,const CRect& rc,COLORREF clr)
    {
     if(pic == NULL)
      return;
     CDC dc;  //内存DC
     dc.CreateCompatibleDC(NULL);
     CBitmap bmp;
     bmp.CreateCompatibleBitmap(&m_MemDC,m_RtDC.Width(),m_RtDC.Height());  //m_RtDC为内存DC区域
     CBitmap* pold = dc.SelectObject(&bmp);
     SIZE sizeInHiMetric;
     pic->get_Width(&sizeInHiMetric.cx);
     pic->get_Height(&sizeInHiMetric.cy); 
     //拷贝整个图片到dc整个客户区
     pic->Render(dc.GetSafeHdc(), 0, 0, m_RtDC.Width(), m_RtDC.Height(),
        0, sizeInHiMetric.cy, sizeInHiMetric.cx, -sizeInHiMetric.cy, NULL);   //把 pic 画到真个dc 上
     //这里的m_MemDC 也是内存DC,拷贝dc 整个区域到m_MemDC 的 rc 区域,clr 为要剔除掉的关键色
     m_MemDC.TransparentBlt(rc.left,rc.top,rc.Width(),rc.Height(),&dc,0,0,m_RtDC.Width(),m_RtDC.Height(),clr);
     bmp.DeleteObject();
     dc.SelectObject(pold);
     dc.DeleteDC();
    }
    最后 m_RealDC 真正的设备 dc, 获取全部 memdc 内容
    m_RealDC->BitBlt(0,0,m_RtDC.Width(),m_RtDC.Height(),&m_MemDC,0,0,SRCCOPY);
    可以在 OnPaint() 和 OnEraseBkgnd(CDC* pDC) 调用该函数DrawPicture 函数,OnEraseBkgnd(CDC* pDC) 注意要 return TRUE;
    使用后不再有闪耀的问题。

    使用对话框打开文件夹获得路径

    一般用CFileDialog 只能打开文件而不能打开文件夹。实现这个功能可以使用windows api  
    LPITEMIDLIST SHBrowseForFolder(LPBROWSEINFO lpbi);
     
     BROWSEINFO bi;
     char path[MAX_PATH];
     LPITEMIDLIST pbi;
     ZeroMemory(&bi,sizeof(BROWSEINFO));          
     bi.pszDisplayName = path;                           //用来存放文件夹名称的缓冲区
     pbi = SHBrowseForFolder(&bi);                      //打开文件夹对话框
     SHGetPathFromIDList(pbi,path);                   //path 就是获得的文件夹路径
     
    September 25

    鼠标HOOK程序

            对于win32每个进程都有自己独立的4GB空间,这个每个程序相对于其他程序都是独立的,一个程序轻易不能访问其他程序地址,一旦访问了轻则跳出出错提示,重则蓝屏,然而当你离开了当前程序,却想要跟踪一些消息,就困难重重了。幸好windws 给我们提供了钩子(hook)函数。
      钩子(hook)一般分为两个等级:全局钩子和局部钩子。全局顾名思义可以挂钩其他程序的消息,而局部则直挂钩使用钩子函数的进程。当然两种都要使用到dll,需要把dll注入到其他进程中,所以使用一定要小心,使用不当会影响其他程序的稳定性。下面先介绍一下windows的钩子函数:
    安装钩子函数:
    HHOOK SetWindowsHookEx( int idHook,HOOKPROC lpfn,HINSTANCE hMod,DWORD dwThreadId);
    IdHook: 钩子类型,有鼠标、键盘、巨集等等10几种
    Lpfn: 挂钩的函数,用来处理拦截消息的函数。必须是全局函数
    HMod: 当前进程的句柄
    dwThreadId: 设置要挂接的线程ID.为NULL则为全局钩子
    卸载钩子函数
    BOOL UnhookWindowsHookEx(HHOOK hhk);
    Hhk 要卸载的钩子句柄
    还有一个函数:
    LRESULT CallNextHookEx(HHOOK hhk,int nCode,WPARAM wParam, LPARAM lParam);
    用于把拦截的消息继续传递下去,不然其他程序可能会得不到相应的消息,在一个按钮上点击,而程序不能执行,是很让人费解的。
    基本写钩子程序主要是用上面的3个函数。下面就在vc下,一步一步写一个鼠标钩子。
    首先当然是创始一个工程了(呵呵,废话),选择 MFC AppWizard(dll),下一步后,选择 MFC Extension Dll,接着创建 mousehook.h 和mousehook.cpp 文件
    mousehook.h 文件内容如下,是不是很短小呢,选择MFC Extension(扩展) Dll就可以使用类,其他普通和常规dll是不能办到的,不过选择了扩展dll,这个dll就只能在 MFC 程序中使用,在vb、delphi中就不能用了:
    // ____________________________________________________________________________
    //
    // 类: CMousehook
    // 目的: 用来挂钩鼠标
    // 描述: 该类会随着钩子客户端关闭而自动卸载
    // ____________________________________________________________________________
    
    #ifndef _MOUSEHOOK_ //控制头文件只包含一次
    #define _MOUSEHOOK_
    #define WM_MOUSE_HOOK WM_USER+110 //传递给钩子dll客户端的消息
    class AFX_EXT_CLASS CMousehook:public CObject
    {
    public:
     CMousehook();
     //析构时自动卸载钩子
     ~CMousehook();
     //安装钩子,把客户程序窗口句柄发给dll,拦截后dll向客户程序发送消息WM_MOUSE_POS
     BOOL installhook(HWND hclientwnd);
     //卸载钩子
     BOOL uninstallhook();
     //鼠标点击次数
     int GetMouseClickCount();
     //包含鼠标位置的数组。为什么使用POINT 结构传递坐标客户端赋值不能编译成功??
     int* GetClickPosition();
    };
    #endif
    下面是相应的实现文件:
    // ==================
    // INCLUDE FILES
    // ==================
    #include "stdafx.h"
    #include
    #include "mousehook.h"
    // ==================
    // DEFINES
    // ==================
    #ifdef _DEBUG
    #define new DEBUG_NEW
    #undef THIS_FILE
    static char THIS_FILE[] = __FILE__;
    #endif
    
    //公共数据,用于不通进程的数据共享。不用进程都装载mousehook.dll,但都放在自己的范围,所以mousehook类成员是不能共享的,必须创建全局数据段。在不同程序之间共享这些数据,
    //还需要在相应的 .def 文件中加入
    SECTIONS
    mymousedata READ WRITE SHARED
    //把该全局数据段设置为读写共享
    //全局共享数据段声明
    #pragma data_seg("mymousedata")
    static HINSTANCE hinst=NULL; //注入钩子的程序句柄
    static HHOOK g_hook=NULL; //钩子句柄
    static HWND g_hwnd=NULL; //需要处理钩子消息的客户端句柄
    static int mouseclickcount=0; //鼠标点击次数
    static int position[2]={0,0};//鼠标的位置信息
    #pragma data_seg()
    
    static AFX_EXTENSION_MODULE MousehookDLL = { NULL, NULL };
    extern "C" int APIENTRY
    DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
    {
     UNREFERENCED_PARAMETER(lpReserved);
     if (dwReason == DLL_PROCESS_ATTACH)
     {
     TRACE0("MOUSEHOOK.DLL Initializing!\n");
     if (!AfxInitExtensionModule(MousehookDLL, hInstance))
     return 0;
     new CDynLinkLibrary(MousehookDLL);
     }
     else if (dwReason == DLL_PROCESS_DETACH)
     {
     TRACE0("MOUSEHOOK.DLL Terminating!\n");
     AfxTermExtensionModule(MousehookDLL);
     }
    
     //传给SetWindowsHookEx()函数的当前程序句柄;
     hinst=hInstance;
     return 1; // ok
    }
     //传递给钩子安装函数参数 lpfn ,一个要注入到各个进程的全局函数。必须声明为 extern “C”
    extern "C" __declspec(dllexport) LRESULT WINAPI mouseproc(int nCode,WPARAM wParam,LPARAM lParam)
    {
     //检测鼠标消息,鼠标钩子消息存放在 wParam 参数中,lParam 指向MOUSEHOOKSTRUCT //的指针,存放鼠标位置,当前窗口句柄,鼠标消息等等。
     if(wParam==WM_MOUSEMOVE||wParam==WM_LBUTTONDOWN||wParam==WM_RBUTTONDOWN)
     {
     LPMOUSEHOOKSTRUCT mousedetials=(LPMOUSEHOOKSTRUCT) lParam;
     //由于c函数不支持结构赋值,使用MOUSEHOOKSTRUCT 变量传递坐标不能成功
     position[0]=mousedetials->pt.x;
     position[1]=mousedetials->pt.y;
     if(wParam==WM_LBUTTONDOWN)
     {
     mouseclickcount++;
     }
    //给钩子客户端发送消息,使客户端刷新数据,使用PostMessage,不要使用//SendMessage, SendMessage要等到函数返回才继续执行。
     PostMessage(g_hwnd,WM_MOUSE_HOOK,0,0);
     }
     //把钩子消息传递下去。
     return CallNextHookEx(g_hook,nCode,wParam,lParam);
    }
    CMousehook::CMousehook()
    {
    }
    CMousehook::~CMousehook()
    {
     uninstallhook();
    }
    BOOL CMousehook::installhook(HWND hclientwnd)
    {
     BOOL bRESULT=FALSE;
     //判断是否安装了一次钩子
     if(g_hook!=NULL)
     {
     bRESULT=true;
     }
     else
     {
     //安装钩子
     g_hook=SetWindowsHookEx(WH_MOUSE,(HOOKPROC)mouseproc,hinst,0);
     if(g_hook!=NULL)
     {
     bRESULT=TRUE;
     }
     else
     {
     //安装钩子失败(可能吗???)
     bRESULT=FALSE;
     MessageBox(NULL,"挂钩失败","Failure",MB_OK);
     }
     }
     //钩子的客户端窗口句柄
     g_hwnd=hclientwnd;
     return bRESULT;
    }
    BOOL CMousehook::uninstallhook()
    {
     BOOL bResult=false;
     if (g_hook!=NULL)
     {
     // 卸载钩子
     bResult=UnhookWindowsHookEx(g_hook);
     if (bResult)
     {
     g_hook=NULL;
     }
     }
     else
     {
     bResult=true;
     }
     return bResult;
    }
    int CMousehook::GetMouseClickCount()
    {
     //返回鼠标点击次数
     return mouseclickcount;
    }
    int* CMousehook::GetClickPosition()
    {
     //返回鼠标坐标
     return position;
    }
    现在完成的是鼠标钩子的动态链接库,经过编译后需要经应用程序的调用才能实现对当前系统下各线程间鼠标消息的拦截处理。这部分同普通动态链接库的使用没有任何区别,在将其使用LoadLibrary 加载到进程后,首先调用动态链接库的Cmousehook: :installhook函数安装好钩子,此时即可对系统下的鼠标消息实施拦截处理。经上述编程,在安装好鼠标钩子后,鼠标在移动到系统任意窗口上时,马上就会通过对鼠标消息的拦截处理而获取到当前鼠标位置。系统钩子具有相当强大的功能,通过这种技术可以对几乎所有的Windows系统消息和事件进行拦截处理。这样向QQ自动隐藏(或者可停靠)功能,都很容易实现。以及一些简单的木马,当判断鼠标当前窗口是密码窗口时,只需要向该窗口发送WM_GETTEXT即可轻松得到密码。这种技术广泛应用于各种自动监控系统对进程外消息的监控处理。本文只对鼠标钩子的一些基本原理和一般的使用方法做了简要的探讨,感兴趣的读者完全可以在本文所述代码基础之上用类似的方法实现对 诸如键盘钩子、外壳钩子等其他类型钩子的安装与使用。本文所述代码在Windows 2000下由Microsoft Visual C++ 6.0编译通过。