前面快乐高四第三十三课,介绍了在控制台程序中怎么利用CChart绘制曲线,那里的方法呢,很简单,就是创建一个弹出窗口,然后在这个弹出窗口上绘图,其实技术含量比较低。
这一课呢,笨笨想给大家介绍一下怎么直接在控制台窗口上绘图。什么,是那个黑不拉几的Dos窗口吗?听到这个想法,你是不是想吐血啊?哈哈!!!!!
在控制台窗口绘图,最困难的是Win32窗口中我们认为理所当然的东西,里面居然没有,比如各种消息!
下面还是开始吧,让我们回到旧石器时代。
第一步,建立一个控制台程序,注意选择“A simple application”,这样IDE会帮忙建立一个空的程序,省一点事。
第二步,引入CChart,并加入要用的头文件。
#include "Chart.h"
#if defined(_UNICODE) || defined(UNICODE)
# pragma comment(lib,"CChartu.lib")
#else
# pragma comment(lib,"CChart.lib")
#endif
using namespace NsCChart;
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <math.h>
#define WM_MOUSEWHEEL 0x020A
extern "C" WINBASEAPI HWND WINAPI GetConsoleWindow();
这里引入了Windows的一个未公开的API,GetConsoleWindow(),用于找到程序的控制台窗口句柄。也可用FindWindow这个API,但比较麻烦。
第三步,添加两个全局变量。
CChart chart;
HWND hWnd;
这里chart用来绘图,不需多说,hWnd用于保存控制台窗口句柄。
第四步,编写绘图子函数。
// 绘图函数
void MyDraw()
{
HDC hDC = GetDC(hWnd);
RECT rt;
GetClientRect(hWnd, &rt);
rt.top = (rt.top+rt.bottom)/2;
chart.OnDraw(hDC, rt);
ReleaseDC(hWnd, hDC);
}
这个套路大家应该都很熟悉,注意这里我们想利用控制台窗口的下半拉画图。
有了全局的HWND句柄,利用GetDC获得HDC,在HDC上绘图。最后必须释放HDC,不然内存会泄露。
第五步,在main()函数的一开始,就初始化hWnd。
hWnd = GetConsoleWindow();
这里就是利用了那个未公开的API获得控制台窗口句柄。
第六步,设置CChart数据。
double pi = 4.0*atan(1.0);
int perioid = 360;
for(int i=0; i<4*perioid; ++i)
{
chart.AddPoint2D(i, 1.4*sin(i*2.0*pi/perioid));
}
chart.SetTitle(_T("在控制台窗口中绘图"));
当然这里随便怎么添加数据都可以的。
第七步,绘图。在上面的代码后面添加:
MyDraw();
printf("控制台窗口绘图!\n");
这里简单调用了前面编写的绘图子函数。由于我们是在窗口下半拉绘图,上半拉仍然可以用于控制台的字符输出,所以这里测试了一下printf输出函数。
效果如图呢!
怎么样?还是不错吧!
第八步,调整一下样式。
这里CChart默认是白背景,控制台窗口是黑背景,有点不协调。那我们添加下面两句话调整一下。
chart.SetBkgndColor(RGB(0, 0, 0));
chart.SetTitleColor(RGB(245, 245, 245));
现在的效果如图。
这下融为一体了。
第九步,添加消息循环,并增加重绘功能。
从输出图像可以看到Press any key to continue字样,老鸟都知道这表示程序已经结束。
另外我们如果把这个控制台窗口隐藏一下再打开,图像就消失了。这不符合我们的需要。
在上面的代码后面添加如下代码。
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
HANDLE Hout = GetStdHandle(STD_OUTPUT_HANDLE);
INPUT_RECORD inp;
DWORD recnum;
bool loop = true;
while(loop)
{
ReadConsoleInput(hIn, &inp, 1, &recnum);
switch(inp.EventType)
{
case FOCUS_EVENT:
MyDraw();
break;
}
}
CloseHandle(hIn);
CloseHandle(Hout);
效果如图。
上面加了一个死循环处理消息。和普通的Windows消息循环不一样,这里用ReadConsoleInput读取消息,在窗口焦点事件FOCUS_EVENT中重绘。
这下解决了上面的问题。注意字符光标在闪烁,表明程序在运行中,并没有结束。
第十步,添加死循环退出机制。
上面的死循环无法退出。虽然Ctrl+C可以退出程序,但这样会影响资源的释放。
添加代码。
case KEY_EVENT:
if(inp.Event.KeyEvent.uChar.AsciiChar=='c' || inp.Event.KeyEvent.uChar.AsciiChar=='C')loop = false;
break;
这下按c键就可以退出了。注意和Ctrl+C的区别,c键只是退出循环,控制台窗口还在,Ctrl+C是连控制台窗口也关闭了。
第十一步,添加鼠标处理消息。
在死循环前加两个用于消息处理的变量。
POINT point;
UINT message;
添加消息处理代码。
case MOUSE_EVENT:
if(inp.Event.MouseEvent.dwEventFlags == 0)//单击(包括按下或释放)
{
if(inp.Event.MouseEvent.dwButtonState == FROM_LEFT_1ST_BUTTON_PRESSED)//左键按下
{
message = WM_LBUTTONDOWN;
}
else if(inp.Event.MouseEvent.dwButtonState == RIGHTMOST_BUTTON_PRESSED)//右键按下
{
message = WM_CONTEXTMENU;
}
else if(inp.Event.MouseEvent.dwButtonState == FROM_LEFT_2ND_BUTTON_PRESSED)//滚轮
{
message = WM_MOUSEWHEEL;
}
//释放的时候:
else if(inp.Event.MouseEvent.dwButtonState == 0)
{
message = WM_LBUTTONUP;
}
}
else if(inp.Event.MouseEvent.dwEventFlags == DOUBLE_CLICK)//双击
{
message = WM_LBUTTONDBLCLK;
}
else if(inp.Event.MouseEvent.dwEventFlags == MOUSE_MOVED)//移动
{
message = WM_MOUSEMOVE;
}
else
{
message = WM_MOUSEMOVE;
}
GetCursorPos(&point);
ScreenToClient(hWnd, &point);
if(chart.OnEvent(hWnd, message, inp.Event.MouseEvent.dwControlKeyState, point.x + (point.y<<16))) MyDraw();
break;
可以看到,这里先把保存在INPUT_RECORD变量中的鼠标事件信息转换为Windows的常规鼠标消息,然后调用CChart的消息处理函数OnEvent来处理消息。另外,INPUT_RECORD变量中并没有保存鼠标的像素位置信息,只有字符输入的行列信息,这里直接读取鼠标的屏幕坐标并转换为控制台窗口的客户区坐标,而鼠标消息的LPARAM变量低16位为客户区X坐标,高十六位为客户区Y坐标,采用移位的方式point.x + (point.y<<16)把point转换为LPARAM。
运行效果如图。
鼠标的各种消息,除了右键菜单外,都可以正常处理。控制台的右键菜单暂时还不知道怎么调出来。
各种对话框窗口也没有问题。
今天的旧石器时代之旅就告一段落了。