Windows и «Модель-Вид-Контроллер
»
Обобщенная Windows программа
Перевод А. И. Легалова
Англоязычный оригинал находится на сервере компании
Эта программа использует набор базовых классов, которые инкапсулируют Windows API
• Controller (Контроллер) — Мост между оконной процедурой и объектно-ориентированным миром.
• View (Вид) — Инкапсулирует вывод Windows программы.
• Canvas (Холст) — Инкапсулирует различные контексты устройств и действия, которые Вы можете сделать с их использованием.
• Model (Модель) — Работник и мозг вашей программы. Вообще не имеет дело с Windows.
Обратите внимание: это Win32 программа — она м.б. запущена под Windows 95, 98, NT, 2000, Me.
Обратите внимание: _set_new_handler — это специфика Microsoft. Если вы используете другой компилятор, то скорее удалите эту строку из кода. Согласно текущему стандарту C++, оператор new должен выбрасывать исключения в любом случае ( VC++ сейчас тоже поддерживает стандарт. А.Л. ).
Обратите внимание: Старые компиляторы могут иметь проблемы с шаблонами ( Вряд ли кто использует такие старые компиляторы для программирования под Windows. А.Л. ). В этом случае вы можете заменить используемые шаблоны типа Win[Get/Set]Long прямыми вызовами Get/SetWindowLong . Например, вместо вызова
Controller * pCtrl = WinGetLong<CONTROLLER *> (hwnd);
вы можете записать
Controller * pCtrl = reinterpret_cast<Controller *> (::GetWindowLong (hwnd, GWL_USERDATA));
Загрузка упакованных исходных текстов Generic (11 кб).
WinMain
При запуске WinMain, создается класс окна и главное окно нашего приложения. Я инкапсулировал эти действия внутри двух классов: WinClass и WinMaker. WinClass может также сообщать нам о том, что уже выполняются другие экземпляры нашей программы. Когда подобное случается в нашем примере, мы просто активизируем уже выполняющийся экземпляр программы и выходим из запускаемого приложения. Так необходимо поступать тогда, когда Вы хотите, чтобы в один момент времени выполнялся только один экземпляр вашей программы.
При успешном создании главного окна, мы входим в цикл обработки сообщений. Обратите внимание, что в этот раз функцией TranslateMessage обрабатываются клавиатурные сообщения. Дело в том, что наша программа имеет пункты меню, к которым можно обращаться, используя комбинации Alt+key.
Другим интересным моментом этой программы является то, что мы больше не используем строки для обозначения наших ресурсов. Мы используем числовые идентификаторы (ids). Более того, мы используем их даже тогда, когда осуществляются API вызовы таких строк, как имя класса окна или заголовок окна. Мы сохраняем все строки в строковых ресурсах и обращаемся к ним через идентификаторы (ids). Ваша среда разработки для Windows скорее всего имеет редактор ресурсов, который позволяет Вам создавать иконки, меню, строковые ресурсы и назначать им соответствующие числовые идентификаторы. Символические имена этих ids сохранены в файле заголовка, сгенерированном таким редактором. В нашем случае он назван "resource.h".
Константа, ID_MAIN, например, ссылается на иконки основной программы (большую и малую в том же самом ресурсе), главное меню, и строку с именем оконного класса. ID_CAPTION ссылается на строку заголовка окна. Такая организация данных поддерживает возможность многократного использования кода, не говоря уже о легкости локализации.
int WINAPI WinMain (HINSTANCE hInst, HINSTANCE hPrevInst, char* cmdParam, int cmdShow) {
_set_new_handler(&NewHandler);
// Using exceptions here helps debugging your program
// and protects from unexpected incidents.
try {
// Create top window class
TopWinClass topWinClass(ID_MAIN, hInst, MainWndProc);
// Is there a running instance of this program?
HWND hwndOther = topWinClass.GetRunningWindow ();
if (hwndOther != 0) {
::SetForegroundWindow(hwndOther);
if (::IsIconic(hwndOther)) ::ShowWindow(hwndOther, SW_RESTORE);
return 0;
}
topWinClass.Register();
// Create top window
ResString caption(hInst, ID_CAPTION);
TopWinMaker topWin(topWinClass, caption);
topWin.Create();
topWin.Show(cmdShow);
// The main message loop
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
Давайте, рассмотрим 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);
}
После того, как оконный класс зарегистрирован системой, Вы можете создать столько окон этого класса, сколько пожелаете. Они, конечно, совместно используют ту же самую оконную процедуру, которая была зарегистрирована классом. Как будет показано дальше, мы можем различать между собой разные экземпляры окна внутри этой процедуры.
WinMaker
Класс 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 id
_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
TopWinMaker::TopWinMaker ((WinClass& winclass, char const* caption) : WinMaker(winClass) {
_style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
_windowName = caption;
}
Классы общего назначения
Прежде, чем идти дальше, рассмотрим некоторые простые классы общего назначения. WinException — нечто, что мы хотим использовать для исключений во время сбоев Windows API. Он заботится о восстановлении кода ошибки Windows. (Между прочим, имеется простой способ преобразовать код ошибки в строку функцией API FormatMessage.)
Класс ResString просто инкапсулирует строку, хранимую в строковых ресурсах вашего приложения.
// The exception class: stores the message and the error code 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;
};
// The out-of-memory handler: throws exception
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");
}
Controller
Контроллер — нервная система отдельного экземпляра окна. Он создается с этим окном, хранится с ним и, в заключение, разрушается вместе с ним. Вы можете помещать любую информацию о состоянии, имеющую отношение к специфическому экземпляру окна в его контроллер. Вообще же, контроллер содержит "Вид", который имеет дело с рисованием на поверхности окна, и он имеет доступ к "Модели", которая является мозгом вашего приложения (все это называется MVC, или образцом "Модель-Вид-Контроллер" ("Model-View-Controller"), изобретенным Smalltalk-программистами.
Если, как это часто бывает, ваше приложение имеет только одно окно верхнего уровня, Вы можете непосредственно включать модель в ее контроллер. Это упрощает управление ресурсами, но ценой усиления связи контроллера с моделью. В больших проектах нужно избегать таких связей. Предпочтительнее использовать внутри контроллера "интеллектуальный" указатель на модель.
Большинство методов контроллера требует дескриптора окна, с которым они взаимодействуют. Этот дескриптор передается с каждым сообщением 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, которая соответствует экземпляру окна. Это так часто случОконная процедура вызывается с указанием дескриптора к окна, к которому направлено данное сообщение. Этот дескриптор однозначно идентифицирует внутреннюю структуру данных Windows, которая соответствует экземпляру окна. Это так часто случается, что мы можем обращаться к этой структуре данных и использовать ее, чтобы сохранить некоторые специфические для экземпляра данные. Имеется типовой безопасный способ доступа к этой структуре. Между прочим, элемент GWL_USERDATA этой структуры гарантированно присутствует во всех окнах, включая окна сообщения, диалоговые окна и даже кнопки.
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: // Have to catch exception in case new throws!
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);
}
Ниже представлены примеры простых реализаций нескольких методов построения контроллеров. Конструктор должен помнить дескриптор окна для более позднего использования, деструктор должен посылать сообщение выхода (quit), метод Size передает его параметр Просмотру (Экрану), и т.д. Мы будем говорить о рисовании в окне немного позже. Теперь, обратите внимание, что контроллер готовит поверхность "Холста" для работы "Вида".
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 () {
// prepare the canvas and let View do the rest
PaintCanvas canvas(_hwnd);
_view.Paint(canvas, _model);
// Notice: The destructor of PaintCanvas called automatically!
}
Когда пользователь выбирает один из пунктов меню, оконная процедура вызывается с сообщением WM_COMMAND. Соответствующий метод контроллера посылает команду, основанную на id команды. Когда Вы создаете меню, используя редактор ресурса, Вы выбираете эти идентификаторы команд для каждого пункта меню. Они сохранены в соответствующем заголовочном файле ("resource.h" в нашем случае), который должен быть включен в исходный файл контроллера.
Наше меню содержит только три пункта с идентификаторами IDM_EXIT, IDM_HELP, и IDM_ABOUT. Диалоговое окно, которое отображается в ответ на IDM_ABOUT, также создано с использованием редактора ресурсов и имеет id IDD_ABOUT. Его процедура диалога — AboutDlgProc.
И, наконец, чтобы отобразить диалоговое окно, нам нужен дескриптор экземпляра приложения. Стандартный способ восстанавить (отыскать) его состоит в том, чтобы обратиться к внутренней структуре данных Windows, используя соответствующий hwnd.
// Menu command processing
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: {
// Instance handle is available through HWND
HINSTANCE hInst = WinGetLong(_hwnd, GWL_HINSTANCE);
:: DialogBox (hInst, MAKEINTRESOURCE(IDD_ABOUT), _hwnd, AboutDlgProc);
}
break;
}
}
View, Canvas
Объект "Вид" (Экранный объект) обычно хранит размеры клиентской области. Они обновляются всякий раз, когда контроллер обрабатывает сообщение WM_SIZE. Первое сообщение WM_SIZE посылается во время создания окна и до посылки WM_PAINT, поэтому мы можем безопасно принимать, его. Когда вызывается Paint, размерности клиентской области уже известны.
Графический вывод к окну осуществляется, вызывом соответствующих методов объекта Canvas. В нашем случае, мы печатаем текст, полученный из модели и рисуем вертикальную строку в десяти пикселах от левого края клиентской области.
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);
}
Объект "Холст" инкапсулирует то, что, на языке Windows, называется Контекстом устройства. Наш Холст очень прост, он знает только, как печатать текст и рисовать линии, но ваш Холст может иметь много больше методов, которые выполняют творческие функции. Мы больше расскажем о Холсте при описании одной из следующих обучающих программ.
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:
// Protected constructor: You can't construct
// a Canvas object, but you may be able
// to construct objects derived from it.
Canvas(HDC hdc): _hdc (hdc) {}
HDC _hdc;
};
Холсты, который Вы создаете, в ответ на сообщение WM_PAINT, имеет специальный вид. Они получают контекст устройства, вызывая BeginPaint и отдают его, вызывая EndPaint. PAINTSTRUCT содержит дополнительную информацию, о некоторой части рабочей области, которая должна быть перерисована, и т.д. Пока мы игнорируем некоторые подробности, но если вы серьезно относитесь к эффективности, Вы должны изучить это более детально.
// Concrete example of canvas.
// Create this object after WM_PAINT message
class PaintCanvas: public Canvas {
public:
// Constructor obtains the DC
PaintCanvas(HWND hwnd) : Canvas (:: BeginPaint (hwnd, &_paint)), _hwnd (hwnd) {}
// Destructor releases the DC
~PaintCanvas () {
:: EndPaint (_hwnd, &_paint);
}
protected:
PAINTSTRUCT _paint;
HWND _hwnd;
};
Далее: Разве можно программировать в Windows без использования элементов управления?