Эта программа использует основной набор классов инкапсулирующих Windows API.
Controller - мост между оконной процедурой и объектно-ориентированным миром.
View - инкапсулирует вывод; Windows программы.
Canvas - инкапсулирует различные контексты устройств и то, что вы можете делать с ним.
Model - руки и мозг вашей программы, которые не имеют дело с Windows совсем.
Давайте начнем с WinMain, там мы создаем оконный класс и окно верхнего уровня нашего приложения. Я скрою действия внутри двух классов: WinClass и WinMaker. WinClass может сказать нам, о том, что уже запущен экземпляр; нашей программы. Когда нечто подобное случается, в нашем примере, мы можем легко активировать подготовленный к запуску экземпляр приложения и выйти. Вы можете делать это, тогда когда захотите, только один экземпляр вашей программы может быть запущен за раз.
Раз окно верхнего уровня прекрасно создано, то мы входим в цикл (петлю) обработки сообщений. Извещение о том, что в этот раз мы обрабатываем клавиатурные вызовы, получаем посредством вызова TranslateMessage. Поскольку наша программа имеет пункты меню, то должно быть доступно использование Alt+[клавиша] комбинаций.
Другая интересная черта этой программы - это то, что мы не будем использовать строки в названии наших ресурсов - мы используем числовые идентификаторы. Более того, когда происходит API вызов для получения строки, наподобие имени оконного класса или заголовка, мы храним строки в строковых ресурсах и доступ к ним получаем через идентификаторы (ids). Ваша среда разработки под Windows очень вероятно имеет редактор ресурсов, что позволяет создавать иконки, меню, и строковые ресурсы и определять им подходящие числовые идентификаторы. Символические имена для этих идентификаторов хранятся в заголовочном файле, созданном таким редактором, в нашем случае - это resource.h.
Постоянная ID_MAIN, например, относится к главным иконкам приложения (большая и маленькая в некотором ресурсе), главному меню, и строке с именем оконного класса. ID_CAPTION относится к строке заголовка окна. Такая организация расширяет возможности повторного использования кода, не говоря уж об удобстве локализации.
int WINAPI WinMain
(HINSTANCE hInst, HINSTANCE hPrevInst,
char * cmdParam, int cmdShow)
{
_set_new_handler (& NewHandler);
// Использование исключений поможет отлаживать вашу программу
// и защитить от неожиданных инцидентов.
try
{
// Создание класса окна верхнего уровня
TopWinClass topWinClass (ID_MAIN, hInst, MainWndProc);
// Действительно ли запущен экземпляр этой программы?
HWND hwndOther = topWinClass.GetRunningWindow ();
if (hwndOther != 0)
{
::SetForegroundWindow (hwndOther);
if (::IsIconic (hwndOther))
::ShowWindow (hwndOther, SW_RESTORE);
return 0;
}
topWinClass.Register ();
// Создаем окно верхнего уровня
ResString caption (hInst, ID_CAPTION);
TopWinMaker topWin (topWinClass, caption);
topWin.Create ();
topWin.Show (cmdShow);
// Главная петля сообщений
MSG msg;
int status;
while ((status = ::GetMessage (&msg, 0, 0, 0)) != 0)
{
if (status == -1)
return -1;
::TranslateMessage (&msg);
::DispatchMessage (&msg);
}
return msg.wParam;
}
catch ( WinException e )
{
char buf [50];
wsprintf (buf, "%s, Error %d", e.GetMessage (), e.GetError ());
::MessageBox (0, buf, "Exception", MB_ICONEXCLAMATION | MB_OK);
}
catch (...)
{
::MessageBox (0, "Unknown", "Exception",
MB_ICONEXCLAMATION | MB_OK);
}
return 0;
}
Давайте взглянем на класс WinClass. Он инкапсулирует вызов Windows-определенной структуры WNDCLASSEX и обеспечивает приемлемые значения по умолчанию для всех его полей. Этот класс является производным от простого класса WinSimpleClass, который вы можете использовать для встраивания некоторых Windows классов (подобных кнопкам, спискам и т.д.).
Я определил, примерные методы, которые могут быть использованы для переопределения значений по умолчанию. Например, SetBgSysColor изменяет цвет фона определенный по умолчанию в клиентской области окна в один из предопределенных системой цвет. Метод SetResIcons загружает соответствующие иконки из ресурсов и закрепляет их за оконным классом. Эти иконки будут потом появляться в верхнем левом углу главного окна и на панели задач Windows.
TopWinClass производный от WinClass позволяет использовать его методы. Он также устанавливает меню вверху оконного класса.
class WinSimpleClass
{
public:
WinSimpleClass (char const * name, HINSTANCE hInst)
: _name (name), _hInstance (hInst)
{}
WinSimpleClass (int resId, HINSTANCE hInst);
char const * GetName () const
{
return _name.c_str ();
}
HINSTANCE GetInstance () const
{
return _hInstance;
}
HWND GetRunningWindow ();
protected:
HINSTANCE _hInstance;
std::string _name;
};
WinSimpleClass::WinSimpleClass (int resId, HINSTANCE hInst)
: _hInstance (hInst)
{
ResString resStr (hInst, resId);
_name = resStr;
}
HWND WinSimpleClass::GetRunningWindow ()
{
HWND hwnd = ::FindWindow (GetName (), 0);
if (::IsWindow (hwnd))
{
HWND hwndPopup = ::GetLastActivePopup (hwnd);
if (::IsWindow (hwndPopup))
hwnd = hwndPopup;
}
else
hwnd = 0;
return hwnd;
}
class WinClass: public WinSimpleClass
{
public:
WinClass (char const * className, HINSTANCE hInst, WNDPROC wndProc);
WinClass (int resId, HINSTANCE hInst, WNDPROC wndProc);
void SetBgSysColor (int sysColor)
{
_class.hbrBackground = reinterpret_cast (sysColor + 1);
}
void SetResIcons (int resId);
void Register ();
protected:
void SetDefaults ();
WNDCLASSEX _class;
};
WinClass::WinClass (char const * className,
HINSTANCE hInst,
WNDPROC wndProc)
: WinSimpleClass (className, hInst)
{
_class.lpfnWndProc = wndProc;
SetDefaults ();
}
WinClass::WinClass (int resId, HINSTANCE hInst, WNDPROC wndProc)
: WinSimpleClass (resId, hInst)
{
_class.lpfnWndProc = wndProc;
SetDefaults ();
}
void WinClass::SetDefaults ()
{
// Provide reasonable default values
_class.cbSize = sizeof (WNDCLASSEX);
_class.style = 0;
_class.lpszClassName = GetName ();
_class.hInstance = GetInstance ();
_class.hIcon = 0;
_class.hIconSm = 0;
_class.lpszMenuName = 0;
_class.cbClsExtra = 0;
_class.cbWndExtra = 0;
_class.hbrBackground = reinterpret_cast (COLOR_WINDOW + 1);
_class.hCursor = ::LoadCursor (0, IDC_ARROW);
}
void WinClass::SetResIcons (int resId)
{
_class.hIcon = reinterpret_cast (
::LoadImage (
_class.hInstance,
MAKEINTRESOURCE (resId),
IMAGE_ICON,
::GetSystemMetrics (SM_CXICON),
::GetSystemMetrics (SM_CYICON),
0));
// Small icon can be loaded from the same resource
_class.hIconSm = reinterpret_cast (
::LoadImage (
_class.hInstance,
MAKEINTRESOURCE (resId),
IMAGE_ICON,
::GetSystemMetrics (SM_CXSMICON),
::GetSystemMetrics (SM_CYSMICON),
0));
}
void WinClass::Register ()
{
if (::RegisterClassEx (&_class) == 0)
throw WinException ("Internal error: RegisterClassEx failed.");
}
class TopWinClass: public WinClass
{
public:
TopWinClass (int resId, HINSTANCE hInst, WNDPROC wndProc);
};
TopWinClass::TopWinClass (int resId,
HINSTANCE hInst, WNDPROC wndProc)
: WinClass (resId, hInst, wndProc)
{
SetResIcons (resId);
_class.lpszMenuName = MAKEINTRESOURCE (resId);
}
Од;нажды зарегистрировав в системе Windows класс, вы можете создать много окон этого класса, если только захотите. Они, конечно, будут совместно использовать некоторую оконную процедуру, которая была зарегистрированная вместе с классом. Мы увидим позже, как можно различать экземпляры окна внутри процедуры.
Класс WinMaker работает в большинстве случаев как WinClass. Его конструктор обеспечивает разумные значения по умолчанию, которые могут быть переопределены посредством вызова специальных методов. Как только все определено, вы вызываете метод Create, который создает окно и метод Show для его отображения. Внимание, в тот момент, когда вы вызываете Create, ваша оконная процедура вызывается с сообщением WM_CREATE .
Окно верхнего уровня, созданное с использованием класса TopWinMaker, который обеспечивает соответствующий стиль и надпись.
class WinMaker
{
public:
WinMaker (WinClass & winClass);
operator HWND ()
{
return _hwnd;
}
void AddCaption (char const * caption)
{
_windowName = caption;
}
void AddSysMenu ()
{
_style |= WS_SYSMENU;
}
void AddVScrollBar ()
{
_style |= WS_VSCROLL;
}
void AddHScrollBar ()
{
_style |= WS_HSCROLL;
}
void Create ();
void Show (int nCmdShow = SW_SHOWNORMAL);
protected:
WinClass & _class;
HWND _hwnd;
DWORD _exStyle; // extended window style
char const * _windowName; // pointer to window name
DWORD _style; // window style
int _x; // horizontal position of window
int _y; // vertical position of window
int _width; // window width
int _height; // window height
HWND _hWndParent; // handle to parent or owner window
HMENU _hMenu; // handle to menu, or child-window ID
void * _data; // pointer to window-creation data
};
WinMaker::WinMaker (WinClass & winClass)
: _hwnd (0),
_class (winClass),
_exStyle (0), // extended window style
_windowName (0), // pointer to window name
_style (WS_OVERLAPPED), // window style
_x (CW_USEDEFAULT), // horizontal position of window
_y (0), // vertical position of window
_width (CW_USEDEFAULT), // window width
_height (0), // window height
_hWndParent (0), // handle to parent or owner window
_hMenu (0), // handle to menu, or child-window identifier
_data (0) // pointer to window-creation data
{
}
void WinMaker::Create ()
{
_hwnd = ::CreateWindowEx (
_exStyle,
_class.GetName (),
_windowName,
_style,
_x,
_y,
_width,
_height,
_hWndParent,
_hMenu,
_class.GetInstance (),
_data);
if (_hwnd == 0)
throw WinException ("Internal error: Window Creation Failed.");
}
void WinMaker::Show (int nCmdShow)
{
::ShowWindow (_hwnd, nCmdShow);
::UpdateWindow (_hwnd);
}
// Makes top overlapped window with caption
class TopWinMaker: public WinMaker
{
public:
TopWinMaker (WinClass & winClass, char const * caption);
};
TopWinMaker::TopWinMaker ((WinClass & winClass, char const * caption)
: WinMaker (winClass)
{
_style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
_windowName = caption;
}
Перед тем как мы пойдем дальше, здесь будут некоторые легкие классы для общей выгоды. WinException - это примерно, то, что мы хотим, он выбросит исключение в любом случае при неудаче с Windows API. Он позаботится о возврате кода ошибки Windows. Для конвертации кода ошибки в строку используется FormatMessage API.
Класс ResString является простой l0;нкапсуляцией хранения строковых ресурсов вашего приложения.
// Класс исключения: сохраняет сообщение и код ошибки
class WinException
{
public:
WinException (char* msg)
: _err (::GetLastError()), _msg(msg)
{}
DWORD GetError() const
{
return _err;
}
char const * GetMessage () const
{
return _msg;
}
private:
DWORD _err;
char * _msg;
};
// Выход за пам 103;ть дескриптора: выбрасывает исключение
int NewHandler (size_t size)
{
throw WinException ( "Out of memory" );
return 0;
}
class ResString
{
enum { MAX_RESSTRING = 255 };
public:
ResString (HINSTANCE hInst, int resId);
operator char const * ()
{
return _buf;
}
private:
char _buf [MAX_RESSTRING + 1];
};
ResString::ResString (HINSTANCE hInst, int resId)
{
if (!::LoadString (hInst, resId, _buf, MAX_RESSTRING + 1))
throw WinException ("Load String failed");
}
Contriller (контроллер) - это нервная система исключительно оконного экземпляра. Он создается вместе с окном, сохраняется с ним и разрушается с ним. Вы можете определить любую информацию характерную для экземпляра окна в его контроллере. В общем, контроллер связан с представлением (view), которое отрисовывает на поверхности окна и он имеет доступ к модели (model), которая является мозгом вашего приложения (это называют MVC, или Model-View-Controller паттерн, изобретенный Smalltalk программистами).
Как это часто бывает, ваше приложение имеет только одно окно верхнего уровня, вы можете непосредственно включить модель (model) в его контроллер (controller). Это упрощает управление ресурсами, но там возникают накладные расходы на соединение контроллера с моделью. В крупных проектах избегают таких соединений — используют (умный) указатель на модель внутри контроллера, что предпочтительно.
Большинство методов контроллера зависят от дескриптора окна, которым они оперируют. Это дескриптор передается с каждым сообщением Windows, но его проще хранить один раз внутри объекта контроллера и использовать его всякий раз когда необходимо. Помните, они находятся в отношении один-к-одному, соответственно между окном экземпляра (и следовательно дескрипторами окна) и объектами контроллера.
class Controller
{
public:
Controller(HWND hwnd, CREATESTRUCT * pCreate);
~Controller ();
void Size (int x, int y);
void Paint ();
void Command (int cmd);
private:
HWND _hwnd;
Model _model;
View _view;
};
Оконная процедура - это главный коммутатор приложения Windows. Вы не вызываете ее из; вашей программы - Windows делает это! Кажды;й раз что-то интересное происходит, Windows отправляет вашей программе сообщения. Это сообщение передается в вашу оконную процедуру. Вы можете обработать его, или вы можете передать его оконной процедуре по умолчанию.
Оконная процедура вызывается с дескриптором окна, которому переданное сообщение направлено. Этот дескриптор однозначно идентифицирует внутреннюю структуру данных Windows, которая соответствует переданному экземпляру окна. К стати, GWL_USERDATA часть этой структуры гарантированно представлена во всех окнах, включая окна сообщений (MessageBox), диалоги и кнопки.
template
inline T WinGetLong (HWND hwnd, int which = GWL_USERDATA)
{
return reinterpret_cast (::GetWindowLong (hwnd, which));
}
template
inline void WinSetLong (HWND hwnd, T value, int which = GWL_USERDATA)
{
::SetWindowLong (hwnd, which, reinterpret_cast (value));
}
Каждый раз Windows вызывает нашу оконную процедуру, мы должны вначале определить объект-контроллер. Помните, что может быть несколько окон разделяющих некоторую оконную процедуру, и мы желаем иметь отдельный контроллер для каждого окна. Как можем мы узнать, какой контроллер использовать, когда мы делаем вызов? Мы находим его посредством дескриптора окна. В этом дескрипторе мы храним указатель на контроллер окна, используем Win[Set/Get]Long функций.
При первом вызове, оконная процедура вызывается с WM_CREATE сообщением. В этот время мы создаем объект контроллер и инициализируем его дескриптором окна и специальной структурой данных называемой CREATESTRUCT, которую мы передаём в распор;яжение Windows. Как только мы имеем контроллер, мы сохраняем указатель на него в соответствующую внутреннюю структуру данных Windows под меткой текущего hwnd (дескриптор окна). В следующий раз оконная процедура вызывается с сообщением отличным от WM_CREATE, мы просто находим указатель на наш контроллер, используя hwnd.
Спокойно отдохнем. Оконная процедура интерпретирует параметры сообщения и вызывает соответствующие методы контроллера.
LRESULT CALLBACK WndProc
(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
Controller * pCtrl = WinGetLong (hwnd);
switch (message)
{
case WM_CREATE:
// Ловим исключение в случае нового исключения!
try
{
pCtrl = new Controller (hwnd,
reinterpret_cast (lParam));
WinSetLong (hwnd, pCtrl);
}
catch (WinException e)
{
::MessageBox (hwnd, e.GetMessage(), "Initialization",
MB_ICONEXCLAMATION | MB_OK);
return -1;
}
catch (...)
{
::MessageBox (hwnd, "Unknown Error", "Initialization",
MB_ICONEXCLAMATION | MB_OK);
return -1;
}
return 0;
case WM_SIZE:
pCtrl->Size (LOWORD(lParam), HIWORD(lParam));
return 0;
case WM_PAINT:
pCtrl->Paint ();
return 0;
case WM_COMMAND:
pCtrl->Command (LOWORD (wParam));
return 0;
case WM_DESTROY:
WinSetLong (hwnd, 0);
delete pCtrl;
return 0;
}
return ::DefWindowProc (hwnd, message, wParam, lParam);
}
Здесь примеры простого выполнения нескольких методов контроллера. Конструктор запоминает дескриптор окна для дальнейшего использования, деструктор отправляет сообщение о выходе. Метод Size передает его аргумент в View, и т. д. Мы говорим об отрисовке окна немного позже. Теперь, заметим, что контроллер подготавливает область рисования для работы View.
Controller::Controller (HWND hwnd, CREATESTRUCT * pCreate)
:_hwnd (hwnd),
_model ("Generic")
{
}
Controller::~Controller ()
{
::PostQuitMessage(0);
}
void Controller::Size (int cx, int cy)
{
_view.SetSize (cx, cy);
}
void Controller::Paint ()
{
// подготавливаем холст и передаём View
PaintCanvas canvas (_hwnd);
_view.Paint (canvas, _model);
// Заметим: деструктор PaintCanvas вызывается автоматически!
}
Когда пользователь выбирает один из пунктов меню, оконная процедура вызывается с сообщением WM_COMMAND. Соответствующий метод контроллера отправляет команду, зависящую от идентификатора (id). Когда вы создаете меню используя ваш редактор ресурсов, вы выбираете эти командные идентификаторы для каждого пункта меню. Они хранятся в соответствующем заголовочном файле (resource.h в нашем случае), он должен быть включен в исходник контроллера.
Наше меню содержит только три пункта с идентификаторами IDM_EXIT, IDM_HELP, и IDM_ABOUT. Диалог, который отображается в ответ на IDM_ABOUT, также создан с использованием редактора ресурсов, и ему дан идентификатор IDD_ABOUT. Его диалоговая процедура - AboutDlgProc.
Наконец, для нормального отображения диалога мы нуждаемся в дескрипторе экземпляра приложения. Обычно, доступ к нему получаn2;т через внутреннюю структуру данных Windows, использованием соответствующего hwnd.
// Обработка команд меню
void Controller::Command (int cmd)
{
switch (cmd)
{
case IDM_EXIT:
::SendMessage (_hwnd, WM_CLOSE, 0, 0L);
break;
case IDM_HELP:
::MessageBox (_hwnd, "Go figure!",
"Generic", MB_ICONINFORMATION | MB_OK);
break;
case IDM_ABOUT:
{
// Дескриптор экземпляра приложения доступен черехз HWND
HINSTANCE hInst = WinGetLong (_hwnd,
GWL_HINSTANCE);
::DialogBox (hInst,
MAKEINTRESOURCE (IDD_ABOUT),
_hwnd,
AboutDlgProc);
}
break;
}
}
Объект представления обычно хранит параметры клиентской области. Они обновляются всякий раз, когда процессы контроллера сообщают WM_SIZE. Первое WM_SIZE сообщение посылается вовремя создания и перед WM_PAINT, так мы можем быть уверены, что к моменту вызова Paint, параметры клиентской области известны.
Графический вывод в окно сделан, посредством вызова соответствующих методов объекта Canvas. В нашем случае, мы отрисовываем текст получаемый из модели и рисуем вертикальную линию на 10 пикселей левее от края клиентской области.
class View
{
public:
void SetSize (int cxNew, int cyNew)
{
_cx = cxNew;
_cy = cyNew;
}
void Paint (Canvas & canvas, Model & model);
protected:
int _cx;
int _cy;
};
void View::Paint (Canvas & canvas, Model & model)
{
canvas.Text (12, 1, model.GetText(), model.GetLen());
canvas.Line (10, 0, 10, _cy);
}
Объект холста инкапсулирует, то что называется контекстом устройства (Device Context) в языке Windows. Наш Canvas - это очень прост, он только знает как печатать текст и рисовать линии, но ваш Canvas может иметь на много больше методов, которые делают творческие вещи.
class Canvas
{
public:
operator HDC ()
{
return _hdc;
}
void Line ( int x1, int y1, int x2, int y2 )
{
::MoveToEx (_hdc, x1, y1, 0);
::LineTo (_hdc, x2, y2);
}
void Text (int x, int y, char const * buf, int cBuf)
{
::TextOut ( _hdc, x, y, buf, cBuf );
}
void Char (int x, int y, char c)
{
::TextOut (_hdc, x, y, & c, 1);
}
protected:
// Защищенный конструктор: Вы не можете строить
// объект типа Canvas, но вам доступно наследование его.
Canvas (HDC hdc): _hdc (hdc) {}
HDC _hdc;
};
Холст (canvas), что вы создаете, в ответ на сообщение WM_PAINT может быть специально изменен. Этот контекст устройства захватывается вызовом BeginPaint и освобождается вызовом EndPaint. PAINTSTRUCT содержит дополнительную информацию о каждой части клиентской области, которая отрисована и др. Здесь мы игнорируем некоторые детали, но если это важно для вас вы могли бы узнать больше об этом.
// Concrete example of canvas.
// Создаем этот объект после сообщения WM_PAINT
class PaintCanvas: public Canvas
{
public:
// Конструктор получает DC
PaintCanvas (HWND hwnd)
: Canvas (::BeginPaint (hwnd, & _paint)),
_hwnd (hwnd)
{}
// Деструктор освобождает DC
~PaintCanvas ()
{
::EndPaint(_hwnd, & _paint);
}
protected:
PAINTSTRUCT _paint;
HWND _hwnd;
};
Хотели бы вы программировать без контролов?
Комментариев нет:
Отправить комментарий