代码改变世界

C语言Windows程序设计-> 第八天-> 滚动条

2012-10-31 20:27  wid  阅读(9311)  评论(13编辑  收藏  举报

对上一天学习的回顾:

  1>.  TextOut函数的使用

    TextOut函数的作用是使用系统当前选择的字体、背景颜色以及正文颜色将一个字符串输出到指定位置, 函数原型:

  BOOL TextOut(
      HDC hdc,                 //设备环境句柄
      int nXStart,             //字符串开始输出的x坐标
      int nYStart,             // 字符串开始输出的y坐标   
      LPCTSTR lpString,        //需要输出的字符串
      int cbString             // 字符串的长度
  );

    当函数调用成功时返回一个非零的值, 调用失败时, 返回值为0。

 

  2>. 取得当前系统字体信息:

    使用GetTextMetrics函数可以取得当前字体信息, 函数原型如下:

  BOOL GetTextMetrics(
    HDC hdc,                   // 设备环境句柄
    LPTEXTMETRIC lptm       // 指向一个TEXTMETRIC结构的指针, 该结构用于存放字体信息。 
  );

    参数二LPTEXTMETRIC指向TEXTMETRIC结构, 在函数调用成功时, 函数将系统当前字体的各种信息复制到TEXTMETRIC结构中。

 

 

Windows滚动条介绍

  滚动条由滚动滑块以及两端的滚动箭头组成, 滚动条的作用是当需要显示的内容超过窗口客户区大小时提供上下/左右的翻页使用户能够完整的阅读显示信息, 滚动条的图示:

 

 

滚动条理论基础

  1>. 上下滚动?

    以垂直方向的滚动条为例, 当用户向下滚动滚动条时目的是想看到下方更多的的信息, 因此我们需要将下方的信息显示出来, 如何显示更多的信息?

    解决方案: 将不需要被显示的信息显示到客户区外, 令信息自动被Windows截掉, 图示说明:

 

    由图示看出, 当用户向下翻动滚动条实际上我们是将起始输出部分的y坐标设为负数, 使已经显示过的信息输出到客户区的上部, 我们知道, 输出到客户区外部的信息会被Windows自动截掉, 所以用户不会再次看到已经显示过的信息, 取而代之的就是下方等待显示的信息, 上翻以及左右翻动的显示思路与下翻相同, 不再介绍。

 

  2>. 如何创建一个带有滚动条的窗口?

     创建带有水平/垂直的滚动条的窗口十分简单, 在CreateWindow函数中说明下即可, CreateWindow函数的原型回顾:

  HWND CreateWindow(
    LPCTSTR lpClassName,               //窗口类名称
    LPCTSTR lpWindowName,              //窗口标题
    DWORD dwStyle,                     //窗口样式
    int x,                             //窗口初始x坐标
    int y,                             //窗口初始y坐标
    int nWidth,                        //窗口初始x方向尺寸
    int nHeight,                       //窗口初始y方向尺寸
    HWND hWndParent,                   //父窗口句柄
    HMENU hMenu,                       //窗口菜单句柄
    HANDLE hlnstance,                  //程序实例句柄
    LPVOID lpParam                     //创建参数
  );

     要窗口带有滚动条的窗口, 只需要在第三个参数

   DWORD dwStyle,                     //窗口样式

      也就是在窗口样式的属性中使用位或( | )运算对相关的标识符进行组合即可得到一个带有垂直/水平滚动条的窗口,

        WS_HSCROLL    //水平滚动条的标识符
        WS_VSCROLL    //垂直滚动条的标识符                

     例如要创建一个既含有垂直滚动条又含有水平滚动条的组合:

    WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL

 

  3>. 对于滚动条, Windows需要做哪些事?

    当带有滚动条的窗口创建好后, Windows就会做一些必要的处理来协助我们更好的使用滚动条, Windows需要做的事情如下:

      1>. 处理滚动条中的所有鼠标消息;

      2>. 当用户单击滚动条时提供被单击部分的轻微闪烁;

      3>. 当用户拖动滑块时在滚动条内移动滑块;

      4>. 当窗口大小被调整时, 自动调整滚动条的长度;

      5>. 向滚动条所在的窗口发送滚动条的相关消息。

 

 

  4>. 我们要做的事情:

    相对于系统我们需要做的事情已经较为轻松了主要有4项任务:

      1>. 初始化滚动条的位置和滚动条的范围;

      2>. 处理系统发来的消息;

      3>. 根据发来的消息重置滑块的位置;

      4>. 根据滚动条消息重绘客户区显示的内容。

 

  5>. 将会收到哪些滚动条消息?

    滚动条消息来源同其他消息一样, 伴随着wParam与lParam消息机制, 当窗口为父窗口时消息的来源为wParam, 此时可忽略lParam的值, lParam用于子窗口消息。

    wParam参数分为两部分, 高位字与低位字, 其中高位字代表用户松开鼠标键时滑块的最终位置, 低位字上代表鼠标在滚动条上的动作, 以一个值的形式表现出来, 同样, 为了方便记忆, 有不同的标识符对这些值进行区分, 这些标识符定义在WINUSER.H头文件中, 以SB_开头, 有关滚动条的消息标识符如下:

#define SB_LINEUP           0        //上翻一个单位
#define SB_LINELEFT         0        //左翻一个单位
#define SB_LINEDOWN         1        //下翻一个单位
#define SB_LINERIGHT        1        //右翻一个单位
#define SB_PAGEUP           2        //上翻一页
#define SB_PAGELEFT         2        //左翻一页
#define SB_PAGEDOWN         3        //下翻一页
#define SB_PAGERIGHT        3        //右翻一页
#define SB_THUMBPOSITION    4        //当鼠标放下滑块时
#define SB_THUMBTRACK       5        //移动滑块时
#define SB_TOP              6        //滑块到了顶端
#define SB_LEFT             6        //滑块到了左端
#define SB_BOTTOM           7        //滑块到了底端
#define SB_RIGHT            7        //滑块到了右端
#define SB_ENDSCROLL        8        //释放鼠标

 

  6>.  需要使用到的新函数:

    ①. SetScrollRange

      SetScrollRange函数的作用是设置所指定滚动条范围的最小值和最大值, 其函数的原型如下:

  BOOL SetScrollRange(
    HWND hWnd,            //窗口句柄
    int nBar,              //被设置的滚动条类型
    int nMinPos,           //滚动条的最小位置
    int nMaxPos,           //滚动条的最大位置
    BOOL bRedraw          //重绘标志
  );

    参数二int nBar为被设置的滚动条类型, SB_HORZ表示该窗口的水平滚动条, SB_VERT表示垂直滚动条;

    参数四BOOL bRedraw指定滚动条是否被重绘以反映变化, 当参数为TRUE, 滚动条被重绘, FALSE则不被重绘。

 

    ②. SetScrollPos

      SetScrollPos函数的作用是设置所指定滚动条中的滚动按钮的位置, 函数原型:

  int SetScrollPos(
    HWND hWnd,     //窗体句柄
    int nBar,        //被设置的滚动条类型
    int nPos,           //滚动条的新位置
    BOOL bRedraw      //重绘标志
  );

 

 

 

实战滚动条

  下面我们尝试着输出一些文字, 使其上下、左右均超过客户区的尺寸, 这样我们就可以实际练习下水平滚动条以及垂直滚动条了,  我们准备了很多行文字, 笔者也不知道到底有多少行, 而且最长的那行文字有多少个也不知道, 我们把他放在一个text.h头文件中, 并计算他到底有多少行以及最长的那行有多少字, 由于文字行数较多, 这里将它在代码框里折叠显示, 定义的头文件如下:

View Code - text.h
#include<string.h>

#define NUMLINES ( (int)(sizeof(statement) / sizeof(statement[0]) ) )        //计算总行数

TCHAR *statement[] = {
    TEXT("没有一种不通过蔑视、忍受和奋斗就可以征服的命运。"),
    TEXT("伟人之所以伟大,是因为他与别人共处逆境时,别人失去了信心,他却下决心实现自己的目标。"),
    TEXT("世上没有绝望的处境,只有对处境绝望的人。"),
    TEXT("当你感到悲哀痛苦时,最好是去学些什么东西。学习会使你永远立于不败之地。"),
    TEXT("给自己定目标,一年,两年,五年,也许你出生不如别人好,通过努力,往往可以改变70%的命运。破罐子破摔只能和懦弱做朋友。"),
    TEXT("知道自己要干什么,夜深人静,问问自己,将来的打算,并朝着那个方向去实现。而不是无所事事和做一些无谓的事。"),
    TEXT("梦想无论怎么模糊,它总潜伏在我们心底,使我们的心境永远得不到宁静,直到梦想成为事实。"),
    TEXT("梦是一种欲望,想是一种行动。梦想是梦与想的结晶。"),
    TEXT("生气,就是拿别人的过错来惩罚自己。原谅别人,就是善待自己。"),
    TEXT("一个实现梦想的人,就是一个成功的人。"),
    TEXT("大事坚持原则,小事学会变通。"),
    TEXT("一个人有钱没钱不一定,但如果这个人没有了梦想,这个人穷定了。"),
    TEXT("不要急于表态不急于表态或发表意见,可以使人对你揣摸猜测。谨慎的沉默是精明人的帮手。一旦表态,你的决定就容易受到批评和非议。如果这些决定是以失败告终,你就会更加倒霉。"),
    TEXT("平凡朴实的梦想,我们用那唯一的坚持信念去支撑那梦想。"),
    TEXT("不要总谈论自己你若总是谈论自己,那么不是吹嘘自己,就是贬低自己;前者是虚荣的表现,后者是卑微的表现。"),
    TEXT("千万不要抱怨,抱怨会使你丢丑。抱怨会使人对你傲慢无礼,并促使别人如你所抱怨的那么做。要赢得别人的帮助,最好的办法是表扬他人。"),
    TEXT("凡事终了时务必小心谨慎,顺利抽身退出要比顺利地进入时更难。"),
    TEXT("很难说什么是办不到的事情,因为昨天的梦想,可以是今天的希望,并且还可以成为明天的现实。"),
    TEXT("当你再也没有什么可以失去的时候,就是你开始得到的时候。"),
    TEXT("生命犹如一片绿叶,随着时间的流逝,慢慢变的枯黄,但他的叶脉还是那么清晰可见。"),
    TEXT("我们可以躲开大家,却躲不开一只苍蝇。生活中使我们不快乐的常是一些芝麻小事。"),
    TEXT("人生的意义不在于拿一手好牌,而在于打好一手坏牌。"),
    TEXT("一个人想平庸,阻拦者很少;一个人想出众,阻拦者很多。不少平庸者与周围人关系融洽,不少出众者与周围人关系紧张。"),
    TEXT("“危机”两个字,一个意味着危险,另外一个意味着机会,不要放弃任何一次努力。"),
    TEXT("世上没有绝望的处境,只有对处境绝望的人。"),
    TEXT("再长的路,一步步也能走完,再短的路,不迈开双脚也无法到达。"),
    TEXT("有志者自有千计万计,无志者只感千难万难。"),
    TEXT("成功与不成功之间有时距离很短只要后者再向前几步。"),
    TEXT("世界会向那些有目标和远见的人让路。"),
    TEXT("世界青睐有雄心壮志的人。成功所依靠的惟一条件就是思考。当你的思维以最高速度运转时,乐观欢快的情绪就会充斥全身。一个人最完美的作品都是在充满愉快、乐观、深情的状态下完成的。学会让自己安静,把思维沉浸下来,慢慢降低对事物的欲望。把自我经常归零,每天都是新的起点,没有年龄的限制,只要你对事物的欲望适当的降低,会赢得更多的求胜机会。"),
    TEXT("学会让自己安静,把思维沉浸下来,慢慢降低对事物的欲望。把自我经常归零,每天都是新的起点,没有年龄的限制,只要你对事物的欲望适当的降低,会赢得更多的求胜机会。"),
    TEXT("杨澜:年轻时候应该能够作一些冒险。年轻时候最大的财富,不是你的青春,不是你的美貌,也不是你充沛的精力,而是你有犯错误的机会。"),
    TEXT("没有十全十美的东西,没有十全十美的人,关键是清楚到底想要什么。得到想要的,肯定会失去另外一部分。如果什么都想要,只会什么都得不到。"),
    TEXT("做自己的决定。然后准备好承担后果。从一开始就提醒自己,世上没有后悔药吃。"),
    TEXT("在你内心深处,还有无穷的潜力,有一天当你回首看时,你就会知道这绝对是真的。"),
    TEXT("不为模糊不清的未来担忧,只为清清楚楚的现在努力。"),
    TEXT("没有一种不通过蔑视、忍受和奋斗就可以征服的命运。"),
    TEXT("伟人之所以伟大,是因为他与别人共处逆境时,别人失去了信心,他却下决心实现自己的目标。"),
    TEXT("世上没有绝望的处境,只有对处境绝望的人。"),
    TEXT("当你感到悲哀痛苦时,最好是去学些什么东西。学习会使你永远立于不败之地。"),
    TEXT("给自己定目标,一年,两年,五年,也许你出生不如别人好,通过努力,往往可以改变70%的命运。破罐子破摔只能和懦弱做朋友。"),
    TEXT("知道自己要干什么,夜深人静,问问自己,将来的打算,并朝着那个方向去实现。而不是无所事事和做一些无谓的事。"),
    TEXT("梦想无论怎么模糊,它总潜伏在我们心底,使我们的心境永远得不到宁静,直到梦想成为事实。"),
    TEXT("梦是一种欲望,想是一种行动。梦想是梦与想的结晶。"),
    TEXT("生气,就是拿别人的过错来惩罚自己。原谅别人,就是善待自己。生气,就是拿别人的过错来惩罚自己。原谅别人,就是善待自己。生气,就是拿别人的过错来惩罚自己。"),
    TEXT("一个实现梦想的人,就是一个成功的人。"),
    TEXT("大事坚持原则,小事学会变通。"),
    TEXT("一个人有钱没钱不一定,但如果这个人没有了梦想,这个人穷定了。"),
    TEXT("不要急于表态不急于表态或发表意见,可以使人对你揣摸猜测。谨慎的沉默是精明人的帮手。一旦表态,你的决定就容易受到批评和非议。"),
    TEXT("平凡朴实的梦想,我们用那唯一的坚持信念去支撑那梦想。"),
    TEXT("不要总谈论自己你若总是谈论自己,那么不是吹嘘自己,就是贬低自己;前者是虚荣的表现,后者是卑微的表现。"),
    TEXT("千万不要抱怨,抱怨会使你丢丑。抱怨会使人对你傲慢无礼,并促使别人如你所抱怨的那么做。要赢得别人的帮助,最好的办法是表扬他人。"),
    TEXT("凡事终了时务必小心谨慎,顺利抽身退出要比顺利地进入时更难。"),
    TEXT("很难说什么是办不到的事情,因为昨天的梦想,可以是今天的希望,并且还可以成为明天的现实。"),
    TEXT("当你再也没有什么可以失去的时候,就是你开始得到的时候。"),
    TEXT("生命犹如一片绿叶,随着时间的流逝,慢慢变的枯黄,但他的叶脉还是那么清晰可见。"),
    TEXT("我们可以躲开大家,却躲不开一只苍蝇。生活中使我们不快乐的常是一些芝麻小事。"),
    TEXT("人生的意义不在于拿一手好牌,而在于打好一手坏牌。"),
    TEXT("一个人想平庸,阻拦者很少;一个人想出众,阻拦者很多。不少平庸者与周围人关系融洽,不少出众者与周围人关系紧张。"),
    TEXT("“危机”两个字,一个意味着危险,另外一个意味着机会,不要放弃任何一次努力。"),
    TEXT("世上没有绝望的处境,只有对处境绝望的人。"),
    TEXT("再长的路,一步步也能走完,再短的路,不迈开双脚也无法到达。"),
    TEXT("有志者自有千计万计,无志者只感千难万难。"),
    TEXT("成功与不成功之间有时距离很短只要后者再向前几步。"),
    TEXT("世界会向那些有目标和远见的人让路。"),
    TEXT("世界青睐有雄心壮志的人。成功所依靠的惟一条件就是思考。当你的思维以最高速度运转时,乐观欢快的情绪就会充斥全身。一个人最完美的作品都是在充满愉快、乐观、深情的状态下完成的。"),
    TEXT("学会让自己安静,把思维沉浸下来,慢慢降低对事物的欲望。把自我经常归零,每天都是新的起点,没有年龄的限制,只要你对事物的欲望适当的降低,会赢得更多的求胜机会。"),
    TEXT("杨澜:年轻时候应该能够作一些冒险。年轻时候最大的财富,不是你的青春,不是你的美貌,也不是你充沛的精力,而是你有犯错误的机会。"),
    TEXT("没有十全十美的东西,没有十全十美的人,关键是清楚到底想要什么。得到想要的,肯定会失去另外一部分。如果什么都想要,只会什么都得不到。"),
    TEXT("做自己的决定。然后准备好承担后果。从一开始就提醒自己,世上没有后悔药吃。"),
    TEXT("在你内心深处,还有无穷的潜力,有一天当你回首看时,你就会知道这绝对是真的。"),
    TEXT("不为模糊不清的未来担忧,只为清清楚楚的现在努力。"),
    TEXT("没有一种不通过蔑视、忍受和奋斗就可以征服的命运。"),
    TEXT("伟人之所以伟大,是因为他与别人共处逆境时,别人失去了信心,他却下决心实现自己的目标。"),
    TEXT("世上没有绝望的处境,只有对处境绝望的人。"),
    TEXT("当你感到悲哀痛苦时,最好是去学些什么东西。学习会使你永远立于不败之地。"),
    TEXT("给自己定目标,一年,两年,五年,也许你出生不如别人好,通过努力,往往可以改变70%的命运。破罐子破摔只能和懦弱做朋友。"),
    TEXT("知道自己要干什么,夜深人静,问问自己,将来的打算,并朝着那个方向去实现。而不是无所事事和做一些无谓的事。"),
    TEXT("梦想无论怎么模糊,它总潜伏在我们心底,使我们的心境永远得不到宁静,直到梦想成为事实。"),
    TEXT("梦是一种欲望,想是一种行动。梦想是梦与想的结晶。"),
    TEXT("生气,就是拿别人的过错来惩罚自己。原谅别人,就是善待自己。"),
    TEXT("一个实现梦想的人,就是一个成功的人。"),
    TEXT("大事坚持原则,小事学会变通。"),
    TEXT("一个人有钱没钱不一定,但如果这个人没有了梦想,这个人穷定了。"),
    TEXT("不要急于表态不急于表态或发表意见,可以使人对你揣摸猜测。谨慎的沉默是精明人的帮手。一旦表态,你的决定就容易受到批评和非议。如果这些决定是以失败告终,你就会更加倒霉。"),
    TEXT("平凡朴实的梦想,我们用那唯一的坚持信念去支撑那梦想。"),
    TEXT("不要总谈论自己你若总是谈论自己,那么不是吹嘘自己,就是贬低自己;前者是虚荣的表现,后者是卑微的表现。"),
    TEXT("千万不要抱怨,抱怨会使你丢丑。抱怨会使人对你傲慢无礼,并促使别人如你所抱怨的那么做。要赢得别人的帮助,最好的办法是表扬他人。"),
    TEXT("凡事终了时务必小心谨慎,顺利抽身退出要比顺利地进入时更难。"),
    TEXT("很难说什么是办不到的事情,因为昨天的梦想,可以是今天的希望,并且还可以成为明天的现实。"),
    TEXT("当你再也没有什么可以失去的时候,就是你开始得到的时候。"),
    TEXT("生命犹如一片绿叶,随着时间的流逝,慢慢变的枯黄,但他的叶脉还是那么清晰可见。"),
    TEXT("我们可以躲开大家,却躲不开一只苍蝇。生活中使我们不快乐的常是一些芝麻小事。"),
    TEXT("人生的意义不在于拿一手好牌,而在于打好一手坏牌。"),
    TEXT("一个人想平庸,阻拦者很少;一个人想出众,阻拦者很多。不少平庸者与周围人关系融洽,不少出众者与周围人关系紧张。"),
    TEXT("“危机”两个字,一个意味着危险,另外一个意味着机会,不要放弃任何一次努力。"),
    TEXT("世上没有绝望的处境,只有对处境绝望的人。"),
    TEXT("再长的路,一步步也能走完,再短的路,不迈开双脚也无法到达。"),
    TEXT("有志者自有千计万计,无志者只感千难万难。"),
    TEXT("成功与不成功之间有时距离很短只要后者再向前几步。"),
    TEXT("世界会向那些有目标和远见的人让路。"),
    TEXT("世界青睐有雄心壮志的人。成功所依靠的惟一条件就是思考。当你的思维以最高速度运转时,乐观欢快的情绪就会充斥全身。一个人最完美的作品都是在充满愉快、乐观、深情的状态下完成的。"),
    TEXT("学会让自己安静,把思维沉浸下来,慢慢降低对事物的欲望。把自我经常归零,每天都是新的起点,没有年龄的限制,只要你对事物的欲望适当的降低,会赢得更多的求胜机会。"),
    TEXT("杨澜:年轻时候应该能够作一些冒险。年轻时候最大的财富,不是你的青春,不是你的美貌,也不是你充沛的精力,而是你有犯错误的机会。"),
    TEXT("没有十全十美的东西,没有十全十美的人,关键是清楚到底想要什么。得到想要的,肯定会失去另外一部分。如果什么都想要,只会什么都得不到。"),
    TEXT("做自己的决定。然后准备好承担后果。从一开始就提醒自己,世上没有后悔药吃。"),
    TEXT("在你内心深处,还有无穷的潜力,有一天当你回首看时,你就会知道这绝对是真的。"),
    TEXT("不为模糊不清的未来担忧,只为清清楚楚的现在努力。")
};

//计算statement所有句子中最长语句的长度
int GetMaxLength()
{
    /*
    *计算statement所有句子中最长语句的长度
    *返回值: int GetMaxLength(void) -> int
    */
    int maxLength = 0 ;
    int i ;
    for( i = 0; i < NUMLINES; i++ )
    {
        if( wcslen(statement[i]) > maxLength )
            maxLength = wcslen(statement[i]) ;
    }
    return maxLength ;
}

 

  在这个头文件中, 其中有两句是十分重要的, 一是计算总行数:

 #define NUMLINES ( (int)(sizeof(statement) / sizeof(statement[0]) ) )        //计算总行数

  另一个是计算最长串字符个数的函数GetMaxLength, 该函数的定义如下:

int GetMaxLength()
{
    /*
    *计算statement所有句子中最长语句的长度
    *返回值: int GetMaxLength(void) -> int
    */
    int maxLength = 0 ;
    int i ;
    for( i = 0; i < NUMLINES; i++ )
    {
        if( wcslen(statement[i]) > maxLength )
            maxLength = wcslen(statement[i]) ;
    }
    return maxLength ;
}

 

  下面编写我们的源文件, LearnScroll.c, 先看一下完整的代码, 稍后我们详细解释, 代码如下:

 

  1 #include<windows.h>
  2 #include"text.h"
  3 
  4 LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ) ;        //声明窗口过程函数
  5 
  6 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow )
  7 {
  8     static TCHAR szAppName[] = TEXT("LearnScroll") ;
  9     HWND hwnd ;
 10     MSG msg ;
 11     WNDCLASS wndclass ;
 12 
 13     //窗口类成员属性
 14     wndclass.lpfnWndProc = WndProc ;
 15     wndclass.style = CS_HREDRAW | CS_VREDRAW ;
 16     wndclass.hInstance = hInstance ;
 17     wndclass.lpszClassName = szAppName ;
 18     wndclass.lpszMenuName = NULL ;
 19     wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH) ;
 20     wndclass.hCursor = LoadCursor(NULL, IDI_APPLICATION) ;
 21     wndclass.hIcon = LoadIcon(NULL, IDC_ARROW) ;
 22     wndclass.cbClsExtra = 0 ;
 23     wndclass.cbWndExtra = 0 ;
 24 
 25     //注册窗口类
 26     if( !RegisterClass(&wndclass) )
 27     {
 28         MessageBox( NULL, TEXT("无法注册窗口类!"), TEXT("错误"), MB_OK | MB_ICONERROR ) ;
 29         return 0 ;
 30     }
 31 
 32     //创建窗口
 33     hwnd = CreateWindow(
 34         szAppName, TEXT("滚动条示例"),
 35         WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
 36         CW_USEDEFAULT, CW_USEDEFAULT,
 37         CW_USEDEFAULT, CW_USEDEFAULT,
 38         NULL, NULL, hInstance, NULL
 39     ) ;
 40 
 41     //显示窗口
 42     ShowWindow( hwnd, iCmdShow ) ;
 43     UpdateWindow( hwnd ) ;
 44 
 45     //获取、翻译、分发消息
 46     while( GetMessage( &msg, NULL, 0, 0 ) )
 47     {
 48         TranslateMessage( &msg ) ;
 49         DispatchMessage( &msg ) ;
 50     }
 51     
 52     return msg.wParam ;
 53 }
 54 
 55 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
 56 {
 57     static int cxChar, cxCaps, cyChar, cyClient, cxClient, iVscrollPos, iHscrollPos ;
 58     //cxChar:平均字符宽度; cxCaps: 大写字母平均宽度; cyChar: 字符高; cyClient、cxClient: 客户区y、x方向尺寸; 
 59     //iVscrollPos: 竖直方向滚动条滑块位置; iHscrollPos: 水平方向滚动条滑块位置
 60 
 61     HDC hdc ;
 62     RECT rect ;            //记录客户区RECT结构
 63     int i, x, y;        //i循环控制, x记录水平方向坐标, y竖直方向坐标
 64     PAINTSTRUCT ps ;
 65     TEXTMETRIC tm ;
 66 
 67     switch(message)
 68     {
 69     case WM_CREATE:        //处理WM_CREATE消息
 70         hdc = GetDC(hwnd) ;
 71         GetTextMetrics( hdc, &tm ) ;    //获取系统字体信息
 72         cxChar = tm.tmAveCharWidth ;    //获取平均宽度
 73         cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;        //大写字母平均宽度
 74         cyChar = tm.tmHeight + tm.tmExternalLeading ;                    //字符高度
 75         ReleaseDC( hwnd, hdc );
 76 
 77         SetScrollRange( hwnd, SB_VERT, 0, NUMLINES - 1, FALSE ) ;        //设置竖直滚动条范围的最小值和最大值
 78         SetScrollRange( hwnd, SB_HORZ, 0, GetMaxLength() - 1, FALSE ) ;    //设置水平滚动条范围的最小值和最大值
 79         SetScrollPos( hwnd, SB_VERT, iVscrollPos, TRUE ) ;        //设置竖直滚动条中的滚动按钮的位置
 80         SetScrollPos( hwnd, SB_HORZ, iHscrollPos, TRUE ) ;        //设置水平定滚动条中的滚动按钮的位置
 81 
 82         return 0 ;
 83 
 84     case WM_SIZE:        //处理WM_SIZE
 85         GetClientRect( hwnd, &rect ) ;
 86         cyClient = rect.bottom ;            //得到客户区y方向尺寸
 87         cxClient = rect.right ;                //得到客户区x方向尺寸
 88         return 0 ;
 89 
 90     case WM_VSCROLL:    //处理垂直滚动条消息
 91         switch( LOWORD(wParam) )
 92         {
 93         case SB_LINEUP:            //上翻一行
 94             iVscrollPos -= 1 ;
 95             break ;
 96 
 97         case SB_LINEDOWN:          //下翻一行
 98             iVscrollPos += 1 ;
 99             break ;
100 
101         case SB_PAGEUP:            //向上翻一整页
102             iVscrollPos -= cyClient / cyChar ;
103             break ;
104 
105         case SB_PAGEDOWN:          //向下翻一整页
106             iVscrollPos += cyClient / cyChar ;
107             break ;
108 
109         case SB_THUMBPOSITION:     //拖动滑块滑块被放下时
110             iVscrollPos = HIWORD(wParam) ;
111             break ;
112 
113         default:
114             break;
115         }
116         iVscrollPos = max( 0, min(iVscrollPos, NUMLINES -1) ) ;
117         if( iVscrollPos != GetScrollPos(hwnd, SB_VERT) )        //当滑块位置改变时重置滑块位置
118         {
119             SetScrollPos( hwnd, SB_VERT, iVscrollPos, TRUE ) ;
120             InvalidateRect( hwnd, NULL, TRUE ) ;                //使客户区无效等待重绘
121         }
122         return 0 ;
123     
124     case WM_HSCROLL:    //处理水平滚动条消息
125         switch( LOWORD(wParam) )
126         {
127         case SB_LINELEFT:        //左翻一行
128             iHscrollPos -= 1 ;
129             break ;
130 
131         case SB_LINERIGHT:        //右翻一行
132             iHscrollPos += 1 ;
133             break ;
134 
135         case SB_PAGELEFT:        //左翻一页
136             iHscrollPos -= cxClient / cxCaps ;
137             break ;
138 
139         case SB_PAGERIGHT:        //右翻一页
140             iHscrollPos += cxClient / cxCaps ;
141             break ;
142 
143         case SB_THUMBPOSITION:    //拖动滑块滑块被放下时
144             iHscrollPos = HIWORD(wParam) ;
145             break ;    
146         
147         default:
148             break ;
149         }
150         iHscrollPos = max( 0, min( iHscrollPos, GetMaxLength() -1 ) ) ;
151         if( iHscrollPos != GetScrollPos( hwnd, SB_HORZ ) )
152         {
153             SetScrollPos( hwnd, SB_HORZ, iHscrollPos, TRUE ) ;
154             InvalidateRect( hwnd, NULL, TRUE ) ;
155         }
156         return 0 ;
157 
158     case WM_PAINT:        //处理WM_PAINT消息
159         hdc = BeginPaint( hwnd, &ps ) ;
160 
161         for( i= 0; i < NUMLINES; i++ )
162         {
163             y = cyChar * ( i -iVscrollPos ) ;
164             x = cxCaps * ( 0 - iHscrollPos ) ;
165             TextOut( hdc, x, y, statement[i], lstrlen(statement[i]) ) ;        //输出文字
166         }
167         
168         EndPaint( hwnd, &ps ) ;
169         return 0 ;
170 
171     case WM_DESTROY:    //处理WM_DESTROY消息
172         PostQuitMessage( 0 ) ;
173         return 0 ;
174     }
175     
176     return DefWindowProc( hwnd, message, wParam, lParam ) ;
177 }

 

  编译运行, 看下成果:

 

  看起来还算不错, 当滚动条向下翻时文字就随着向上滚动, 使下面的文字能够显示出来, 水平的滚动条也是这样, 下面详细说说重点部分的代码:

    1>. 创建一个带有垂直滚动条以及水平滚动条的窗口:

  hwnd = CreateWindow(
        szAppName, TEXT("滚动条示例"),
        WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
        CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT,
        NULL, NULL, hInstance, NULL
    ) ;

    

    2>. 设置垂直滚动条、水平滚动条的范围以及初始位置:

        SetScrollRange( hwnd, SB_VERT, 0, NUMLINES - 1, FALSE ) ;          //设置垂直滚动条范围的最小值和最大值
        SetScrollRange( hwnd, SB_HORZ, 0, GetMaxLength() - 1, FALSE ) ;     //设置水平滚动条范围的最小值和最大值
        SetScrollPos( hwnd, SB_VERT, iVscrollPos, TRUE ) ;              //设置垂直滚动条中的滚动按钮的位置
        SetScrollPos( hwnd, SB_HORZ, iHscrollPos, TRUE ) ;              //设置水平定滚动条中的滚动按钮的位置

     可以看出, 垂直滚动条的范围为0到行数-1, 这就意味这, 每下翻/上翻一个单位, 客户区显示的文字就会向上//向下滚动一行; 

    水平滚动条的范围为0到最长那行文字的长度-1, 每左翻/右翻一个单位, 客户区显示的文字就会向右//向左滚动一个字符的宽度

 

    3>. 当窗口大小调整时重新获取客户区尺寸数据:

      case WM_SIZE:        //处理WM_SIZE
          GetClientRect( hwnd, &rect ) ;
          cyClient = rect.bottom ;              //得到客户区y方向尺寸
          cxClient = rect.right ;                //得到客户区x方向尺寸
          return 0 ;

 

    4>. 处理滚动条消息:

      case WM_VSCROLL:    //处理水平滚动条消息
          switch( LOWORD(wParam) )
          {
        case 滚动条消息:
          [处理滚动条消息]
          }

 

    5>. 重置滚动条滑块位置:

        iVscrollPos = max( 0, min(iVscrollPos, NUMLINES -1) ) ;        //确保滚动条的位置在设置的范围内。

        if( iVscrollPos != GetScrollPos(hwnd, SB_VERT) )            //当滑块位置改变时重置滑块位置
        {
            SetScrollPos( hwnd, SB_VERT, iVscrollPos, TRUE ) ;
            InvalidateRect( hwnd, NULL, TRUE ) ;                    //使客户区无效等待重绘
        }
        return 0 ;    

      这一句注释上已经描述的是否清楚了, 当滑块位置改变时重置滑块位置并使客户区无效等待重绘。

 

    6>. 处理重绘消息:

    case WM_PAINT:        //处理WM_PAINT消息
          hdc = BeginPaint( hwnd, &ps ) ;

          for( i= 0; i < NUMLINES; i++ )
          {
              y = cyChar * ( i -iVscrollPos ) ;
              x = cxCaps * ( 0 - iHscrollPos ) ;
              TextOut( hdc, x, y, statement[i], lstrlen(statement[i]) ) ;        //输出文字
          }
        
          EndPaint( hwnd, &ps ) ;
          return 0 ;

 

      目的是重绘客户区内容并使其有效, 注意这里的

            y = cyChar * ( i -iVscrollPos ) ;
            x = cxCaps * ( 0 - iHscrollPos ) ;

      这是计算从起始输出的坐标, 每行对应一个y值, 当滑块的位置向下滚动1时, y的值就会减去一个字符的高度, 使该行显示到窗口外部, 这样新的一行就会被显示出来, 同样, 翻动一整夜的计算思路同一行; x是指水平起始输出位置, 计算思路相同。

 

这样, 一个简单的带有滚动条的窗口就完成了! 看起来挺不错的, 不是吗? 可以先稍微休息一下, 下面我们还有事要做。

 

 

但这还不够好!

  在上面我们使用的滚动条中, 虽说能够滚动文字, 但是依然存在许多小问题:

    问题一滑块的大小是固定的, 而我见到的应用软件滑块能够根据内容的多少自动调整滑块大小;

    问题二: 当我拖动滑块时只有当滑块释放时页面才会滚动, 我想要的是当滑块被拖动时页面也同样跟着滚动;

    问题三: 当滑块滚动到底部时最后一行显示到了客户区顶部, 下面留有一大片的空白, 而我并不需要保留下面的空白, 也就是说最后一行在滑块拖动到底部后它只显示在底部就行。

 

  幸运的是, 我们依然有解决方案:

     问题一: 自行设置滑块的大小;

     问题二根据SB_THUMBTRACK消息处理页面的滚动, SB_THUMBTRACK消息是当滑块被拖动时就会源源不断的发来;

        问题三重新设置滚动的范围。

 

 

 

更好的滚动条:

  在使用更好的滚动条之前我们首先要认识三个新函数: SetScrollInfo、GetScrollInfo以及ScrollWindow.

  1>. SetScrollInfo

    函数功能: 用于设置滚动条的相关参数, 包括滚动范围的最大值和最小值, 页面大小, 滑块的位置, 函数的原型:

  int SetScrollInfo(
    HWND hWnd;              //窗口句柄
    int fnBar,               //指定被设定参数的滚动条的类型
    LPSCROLLINFO lpsi,       //指向一个SCROLLINFO结构
    BOOL fRedraw             //重绘标志
  ) ;

  穿插讲述: 什么是SCROLLINFO结构?

    SCROLLINFO的成员记录有关滚动条的信息, 其结构定义如下:

  typedef struct tagSCROLLINFO 
  { 
    UINT cbSize ;             //设置为sizeof (SCROLLINFO), 表示该结构的大小
    UINT fMask ;             //要设置或获取的值
    int nMin ;               //滚动条范围的最小值
    int nMax ;               //滚动条范围的最大值
    UINT nPage ;            //页面大小
    int nPos ;               //当前位置 
    int nTrackPos ;           //当前追踪位置 
  }SCROLLINFO;

    成员一UINT cbSize :  该参数必须在函数调用之前设置,  cbSize表示该结构的大小, 用sizeof (SCROLLINFO)表示即可。

    成员二UINT fMask: 用于指定指定结构中的哪些成员是有效的, 通过位或运算进行组合可组合的标识符如下:

  SIF_ALL                     //整个结构都有效
  SIF_DISABLENOSCROLL
//禁用滚动条   SIF_PAGE      //用于指定或获取页面的大小, 在SetScrollInfo中用于设定页面的大小, 在GetScrollInfo用于获取页面的大小
  SIF_POS      
//设置/取得滚动条滑块当前的位置
  SIF_RANGE     
//滚动条的范围   SIF_TRACKPOS //仅在GetScrollInfo函数中使用, 并且仅用在处理SB_THUMBTRACK或者SB_THUMBPOSITION的WM_VSCROLL消息或WM_HSCROLL消息时使用。取得当前滑块的跟踪位置。

     穿插讲述完毕! 继续讲解第二个新函数。

 

  2>. GetScrollInfo

    用于取得滚动条的相关参数, 包括滚动范围的最大值和最小值, 页面大小, 滑块的位置, 函数的原型:

  BOOL GetScrollInfo( 
    HWND hWnd,                    //窗口句柄
    int fnBar,                     //指定被设定参数的滚动条的类型
    LPSCROLLINFO lpsi              //指向一个SCROLLINFO结构
   );

 

  3>. ScrollWindow

    该函数的作用是滚动所指定的窗口客户区域内容, 原型如下:

  BOOL ScrollWindow(
    HWND hWnd,               //窗口句柄
    int XAmount,                //指定水平滚动的距离
      int YAmount,                //指定垂直滚动的距离
      CONST RECT *IpRect,         //指向RECT结构的指针, 该结构指定了将要滚动的客户区范围。若此参数为NULL,则整个客户区域将被滚动。 
    CONST RECT *lpClipRect      //指向RECT结构的指针, 该结构指定了要滚动的裁剪区域。只有这个矩形中才会被滚动。
);

  好了, 说的差不多够多了, 研究代码才是更好的沟通方式, 下面我们实际实践一下更好的滚动条, 更多的细节请在代码中体会, 限于篇幅的长度, 这里将WinMain函数折叠显示, 仅将窗口过程函数部分的代码全部显示出来:

View Code - Function - WinMain
 1 #include<windows.h>
 2 #include"text.h"
 3 
 4 LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM ) ;        //声明窗口过程函数
 5 
 6 int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow )
 7 {
 8     static TCHAR szAppName[] = TEXT("LearnScroll") ;
 9     HWND hwnd ;
10     MSG msg ;
11     WNDCLASS wndclass ;
12 
13     //窗口类成员属性
14     wndclass.lpfnWndProc = WndProc ;
15     wndclass.style = CS_HREDRAW | CS_VREDRAW ;
16     wndclass.hInstance = hInstance ;
17     wndclass.lpszClassName = szAppName ;
18     wndclass.lpszMenuName = NULL ;
19     wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH) ;
20     wndclass.hCursor = LoadCursor(NULL, IDI_APPLICATION) ;
21     wndclass.hIcon = LoadIcon(NULL, IDC_ARROW) ;
22     wndclass.cbClsExtra = 0 ;
23     wndclass.cbWndExtra = 0 ;
24 
25     //注册窗口类
26     if( !RegisterClass(&wndclass) )
27     {
28         MessageBox( NULL, TEXT("无法注册窗口类!"), TEXT("错误"), MB_OK | MB_ICONERROR ) ;
29         return 0 ;
30     }
31 
32     //创建窗口
33     hwnd = CreateWindow(
34         szAppName, TEXT("滚动条示例"),
35         WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
36         CW_USEDEFAULT, CW_USEDEFAULT,
37         CW_USEDEFAULT, CW_USEDEFAULT,
38         NULL, NULL, hInstance, NULL
39     ) ;
40 
41     //显示窗口
42     ShowWindow( hwnd, iCmdShow ) ;
43     UpdateWindow( hwnd ) ;
44 
45     //获取、翻译、分发消息
46     while( GetMessage( &msg, NULL, 0, 0 ) )
47     {
48         TranslateMessage( &msg ) ;
49         DispatchMessage( &msg ) ;
50     }
51     
52     return msg.wParam ;
53 }

 

  窗口过程部分的代码:

  1 LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
  2 {
  3     static int cxChar, cxCaps, cyChar, cyClient, cxClient, iVscrollPos, iHscrollPos ;
  4     //cxChar:平均字符宽度; cxCaps: 大写字母平均宽度; cyChar: 字符高; cyClient、cxClient: 客户区y、x方向尺寸; 
  5     //iVscrollPos: 竖直方向滚动条滑块位置; iHscrollPos: 水平方向滚动条滑块位置
  6     HDC hdc ;
  7     RECT rect ;            //记录客户区RECT结构
  8     int i, x, y;        //i循环控制, x记录水平方向坐标, y竖直方向坐标
  9     PAINTSTRUCT ps ;
 10     TEXTMETRIC tm ;
 11     SCROLLINFO  si ;    //SCROLLINFO结构对象
 12     int iMaxLength ;    //所有语句中的最大长度
 13 
 14     iMaxLength = GetMaxLength() ;        //取得最大长度
 15 
 16     switch(message)
 17     {
 18     case WM_CREATE:        //处理WM_CREATE消息
 19         hdc = GetDC(hwnd) ;
 20         GetTextMetrics( hdc, &tm ) ;    //获取系统字体信息
 21         cxChar = tm.tmAveCharWidth ;    //获取平均宽度
 22         cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;        //大写字母平均宽度
 23         cyChar = tm.tmHeight + tm.tmExternalLeading ;                    //字符高度
 24         ReleaseDC( hwnd, hdc );
 25
 26         return 0 ;
 27 
 28     case WM_SIZE:        //处理WM_SIZE
 29         GetClientRect( hwnd, &rect ) ;
 30         cxClient = LOWORD (lParam) ;
 31         cyClient = HIWORD (lParam) ;
 32 
 33         //设置垂直滚动条相关参数
 34         si.cbSize = sizeof (si) ;
 35         si.fMask  = SIF_RANGE | SIF_PAGE ;
 36         si.nMin   = 0 ;
 37         si.nMax   = NUMLINES - 1 ;
 38         si.nPage  = cyClient / cyChar ;
 39         SetScrollInfo(hwnd, SB_VERT, &si, TRUE) ;
 40 
 41         //设置水平滚动条相关参数
 42         si.cbSize = sizeof (si) ;
 43         si.fMask  = SIF_RANGE | SIF_PAGE ;
 44         si.nMin   = 0 ;
 45         si.nMax   = 2 + iMaxLength ;
 46         si.nPage  = cxClient / cxCaps ;
 47         SetScrollInfo(hwnd, SB_HORZ, &si, TRUE) ;
 48 
 49         return 0 ;
 50 
 51     case WM_VSCROLL:    //处理垂直滚动条消息
 52         si.cbSize = sizeof (si) ;
 53         si.fMask  = SIF_ALL ;
 54         GetScrollInfo(hwnd, SB_VERT, &si) ;
 55 
 56         iVscrollPos = si.nPos ;    //记录当前滑块位置
 57 
 58         switch( LOWORD(wParam) )    //处理滚动条消息
 59         {
 60         case SB_TOP:            //到达顶部
 61             si.nPos = si.nMin ;
 62             break ;
 63                
 64         case SB_BOTTOM:            //到达底部
 65             si.nPos = si.nMax ;
 66             break ;
 67 
 68         case SB_LINEUP:            //上翻一行
 69             si.nPos -= 1 ;
 70             break ;
 71 
 72         case SB_LINEDOWN:        //下翻一行
 73             si.nPos += 1 ;
 74             break ;
 75 
 76         case SB_PAGEUP:            //向上翻一整页
 77             si.nPos -= si.nPage ;
 78             break ;
 79 
 80         case SB_PAGEDOWN:        //向下翻一整页
 81             si.nPos += si.nPage ;
 82             break ;
 83 
 84         case SB_THUMBTRACK:        //移动滑块时
 85             si.nPos = si.nTrackPos ;
 86             break ;
 87 
 88         default:
 89             break;
 90         }
 91         si.fMask = SIF_POS ;
 92         SetScrollInfo(hwnd, SB_VERT, &si, TRUE) ;        //重置滑块位置
 93         GetScrollInfo(hwnd, SB_VERT, &si) ;
 94         if( si.nPos != iVscrollPos )
 95         {
 96                ScrollWindow(hwnd, 0, cyChar * (iVscrollPos - si.nPos), NULL, NULL) ;        //滚动内容
 97                UpdateWindow(hwnd) ;
 98         }
 99         return 0 ;
100     
101     case WM_HSCROLL:    //处理水平滚动条消息
102         si.cbSize = sizeof (si) ;
103         si.fMask  = SIF_ALL ;
104         GetScrollInfo (hwnd, SB_HORZ, &si) ;
105 
106         iHscrollPos = si.nPos ;    //记录当前滑块位置
107         switch( LOWORD(wParam) )
108         {
109         case SB_TOP:            //到达顶部
110             si.nPos = si.nMin ;
111             break ;
112                
113         case SB_BOTTOM:            //到达底部
114             si.nPos = si.nMax ;
115             break ;
116             
117         case SB_LINELEFT:        //左翻一行
118             si.nPos -= 1 ;
119             break ;
120 
121         case SB_LINERIGHT:        //右翻一行
122             si.nPos += 1 ;
123             break ;
124 
125         case SB_PAGELEFT:        //左翻一页
126             si.nPos -= si.nPage ;
127             break ;
128 
129         case SB_PAGERIGHT:        //右翻一页
130             si.nPos += si.nPage ;
131             break ;
132 
133         case SB_THUMBTRACK:        //移动滑块时
134             si.nPos = si.nTrackPos ;
135             break ;    
136         
137         default:
138             break ;
139         }
140         si.fMask = SIF_POS ;
141         SetScrollInfo(hwnd, SB_HORZ, &si, TRUE) ;        //重置滑块位置
142         GetScrollInfo(hwnd, SB_HORZ, &si) ;
143         if( si.nPos != iHscrollPos )
144         {
145                ScrollWindow(hwnd, cxCaps * (iHscrollPos - si.nPos), 0, NULL, NULL) ;        //滚动内容
146                UpdateWindow(hwnd) ;
147         }
148         return 0 ;
149 
150     case WM_PAINT:        //处理WM_PAINT消息
151         hdc = BeginPaint( hwnd, &ps ) ;
152 
153         si.cbSize = sizeof (si) ;
154         si.fMask  = SIF_POS ;
155         GetScrollInfo(hwnd, SB_VERT, &si) ;
156         iVscrollPos = si.nPos ;                    //获取当前垂直滑块位置
157 
158         GetScrollInfo(hwnd, SB_HORZ, &si) ;
159         iHscrollPos = si.nPos ;                    //获取当前水平滑块位置
160 
161         for( i= 0; i < NUMLINES; i++ )
162         {
163             y = cyChar * ( i - iVscrollPos ) ;
164             x = cxCaps * ( 0 - iHscrollPos ) ;
165             TextOut( hdc, x, y, statement[i], lstrlen(statement[i]) ) ;        //输出文字
166         }
167         
168         EndPaint( hwnd, &ps ) ;
169         return 0 ;
170 
171     case WM_DESTROY:    //处理WM_DESTROY消息
172         PostQuitMessage( 0 ) ;
173         return 0 ;
174     }
175     
176     return DefWindowProc( hwnd, message, wParam, lParam ) ;
177 }

 

看一下成果:

嗯, 这样看起来就好多了, 如果嫌行间距太挤的话我们可以调节字符的高度

        cyChar = tm.tmHeight + tm.tmExternalLeading ;                    //字符高度

使行间距增大些, 这样看起来会更舒服。

好了, 到这里, 一个较为完善的滚动条就完成了。

 

--------------------

 

wid, 2012.10.31

 

上一篇: C语言Windows程序设计->第七天->TextOut与系统字体