

Программирование на языке MFC
Мой второй блог в серии программирования
Во-первых, для того чтобы рассмотреть вопрос отображения документа, нам необходимо вспомнить, что у нас до сих пор указатель на информацию времени исполнения окна представления нашей программы равен NULL. В связи с этим необходимо описать класс окна представления..Следрвательно, наша программа немного изменилась. Сразу после описания класса документа я добавил описание класса окна представления:
class CDocView : public CView {
DECLARE_DYNCREATE ( CDocView ) public:
CDocView();
virtual -CDocView();
>;
IMPLEMENT_DYNCREATE( CDocView, CView )
CDocView::CDocView()
{
}
CDocView::-CDocView()
{
}
Кроме этого, я изменил метод lnitlnstance() класса CDocViewl:
BOOL CDocViewlApp::Initlnstance() {
#ifdef _AFXDLL
Enable3dControls(); #else
Enable3dControlsStatic(); #endif
LoadStdProfileSettings(); CDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate( IDR_DOCUMENT,
RUNTIME__CLASS ( CDoc ) , RUNTIME_CLASS( CMDIChildWnd ), RUNTIME_CLASS( CDocView ) ) ; AddDocTemplate( pDocTemplate ); CMainFrame* pMainFrame = new CMainFrame; pMainFrame->LoadFrame( IDR_RESOURCE ); m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow ); pMainFrame->UpdateWindow();
return TRUE;
}
Когда я постарался откомпилировать эту программу, я получил сообщение о том, что у класса, наследуемого от CView, не определен метод OnDraw(). Ну, конечно же! Откуда же наш произвольный класс окна представления будет знать, каким образом ему необходимо перерисовываться? Давайте опять постараемся рассуждать логически. Какое сообщение получает окно, когда ему нужно перерисовать себя? Правильно, WM_PAINT. Но для обработки сообщения WM_PAINT объекты MFC используют метод OnPaintQ.
Не является исключением и объекты класса CView и унаследованных от него. Исходный код этого метода находится в файле viewcore.cpp:
void CView::OnPaint() {
// standard paint routine CPaintDC dc(this); OnPrepareDC(&dc); OnDraw(&dc);
}
Очевидно, что перерисовка осуществляется методом OnDraw(). Взглянем на исходный код этого метода, который также находится в файле viewcore.cpp:
void CView::OnDraw(CDC*)
{
}
To, что этот метод не делает ничего, подтверждает нашу догадку о том, что нам нужно переписать именно этот метод. Что ж описание нашего класса CDocView придется немного изменить. Теперь оно будет выглядеть так:
class CDocView : public CView {
DEСLARE_DYNCREATE ( CDocView ) public:
CDocView();
virtual ~CDocView(); void OnDraw( CDC* pDC );
};
Естественно, нам придется переопределить и добавить в программу метод OnDravtr(). Пока этот метод нас не очень интересует, поэтому оставим его пустым:
void CDocView::OnDraw( CDC* pDC )
{
}
Наверное, отсюда можно сделать вывод, что класс CView предоставляет только базовые возможности для отображения данных. Надеюсь, у нас еще будет шанс разобраться с этим.
Любознательный читатель уже, наверное, приготовил мне вопрос: да, конечно, все что рассказывается, это верно, но каким же образом создается окно представления и как оно относится к окну фрейма и документу?
читать отзывы (0)
На что необходимо обратить внимание? К этому моменту мы еще не представляем характера взаимодействия между документом и фреймом. Поэтому просто предположим, что каким-то образом наш документ будет отображаться в рамках фрейма. Давайте поразмыслим, уважаемый читатель. Мы пишем программу, которая будет работать с многодокументным интерфейсом. Наша программа должна отображать данные в одном из дочерних окон многодокументного интерфейса. Следовательно, логично будет в качестве фрейма использовать окно класса CMDIChildWnd. Давайте так и поступим, уважаемый читатель. Итак, ниже я привожу текст метода CDocView1App::lnitlnstance() нашей программы:
BOOL CDocViewlApp::Initlnstance () {
#ifdef _AFXDLL
Enable3dControls(); #else
Enable3dControlsStatic(); #endif
LoadStdProfileSettings(); CDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate( IDR_DOCUMENT,
RUNTIME_CLASS( CDoc ),% RUNTIME_CLASS( CMDIChildWnd ),
NULL );
AddDocTemplate( pDocTemplate ); CMainFrame* pMainFrame = new CMainFrame; pMainFrame->LoadFrame( IDR_RESOURCE ); m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow ); pMainFrame->UpdateWindow();
return TRUE;
}
создается фрейм этого документа. Нетрудно догадаться, что создание фрейма происходит при вызове метода CDocTemplate:: CreateNewFrame():
CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pDoc,
CFrameWnd* pother)
{
if (pDoc != NULL)
ASSERT_VALID(pDoc); // create a frame wired to the specified document
ASSERT(m_nIDResource != 0); // must have a resource. ID
// to load from
CCreateContext context;
context,m_pCurrentFrame = pother;
context.m_pCurrentDoc = pDoc;
context,m_pNewViewClass = m_pViewClass;
context.m_pNewDocTemplate = this;
if (m_pFrameClass == NULL) ‘ {
TRACEO("Error: you must override
CDocTemplate::CreateNewFrame.\n") ;
ASSERT(FALSE); return NULL;
}
CFrameWnd* pFrame =
(CFrameWnd*)m_pFrameClass->CreateObject() ; if (pFrame == NULL) {
TRACE1("Warning: Dynamic create of frame %hs failed.\n",
m_pFrameClass->m_lpszClassName);
return NULL;
}
ASSERT_KINDOF(CFrameWnd, pFrame);
if (context.m_pNewViewClass == NULL)
TRACEO("Warning: creating frame with no default
view.\n");
// create new from resource
if (!pFrame->LoadFrame(m_nIDResource,
WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, // default frame styles
NULL, &context))
{
TRACEO("Warning: CDocTemplate couldn’t create
a frame.\n"); // frame will be deleted in PostNcDestroy cleanup return NULL;
}
// it worked ! return pFrame;
}
Мне бы хотелось обратить внимание читателя на то, что в качестве аргументов методу передаются указатель на документ и указатель (пока нулевой), в который будет записан указатель на созданный фрейм. Мы уже однажды (при рассмотрении метода DoPromptFileName()) замечали, что методу передаются указатели, которые с первого взгляда совершенно не нужны для работы метода. Кажется, здесь тот же случай – ну зачем, скажите, пожалуйста, при создании фрейма знать указатель на документ? То, что буквально в первых строках метода осуществляется проверка того, не равен ли идентификатор ресурсов нулю, говорит о том, что, вероятнее всего, при создании фрейма опять будут использоваться ресурсы. Но, как говорится, поживем – увидим.
В заключение необходимо привести пример использования строки ресурсов для формирования диалога открытия файла. В файл ресурсов я добавил строку
IDRJDOCUMENT "\nExe-file\nExe-file\n
Executable files (*.exe)\n.exe\n\n"
Мне бы хотелось, чтобы в диалоговом окне мне предлагалось осуществить выбор из ехе-файлов. Естественно, мне необходимо указать идентификатор ресурса в методе lnitlnstance() моего приложения. После внесенных изменений он стал выглядеть следующим образом:
BOOL CDocViewlApp::Initlnstance () {
#ifdef _AFXDLL
Enable3dControls(); #else
Enable3dControlsStatic (); #endif
LoadStdProfileSettings(); CDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate( IDRJDOCUMENT,
NULL, NULL, NULL );
AddDocTemplate( pDocTemplate ); CMainFrame* pMainFrame = new CMainFrame; pMainFrame->LoadFrame( IDR_RESOURCE );
m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow ) ; pMainFrame->UpdateWindow() ;
return TRUE;
}
Посмотрим, правильны ли мои рассуждения и сработает ли в данном случае моя программа. Итак, компилируем… Запускаем… Выбираем «File», «Ореп» и…
Окно, появившееся на отображении, вы видите на 13. © Очень надеюсь, что того материала, который мы только что изучили, достаточно для понимания того, что происходит при подготовке стандартного диалога. Надеюсь, что и в этом случае мы успешно решили все поставленные задачи – мы поняли «физику» процесса и осознали возможную степень влияния программиста на процесс создания диалогового окна. Конечно, я мог бы объяснять все мелочи, встречающиеся в исходном коде, например, отображение диалогового окна при помощи метода DoModal(), но имеет ли это смысл?
После приведенных изменений метод CDocView1::lnitlnstance() имел следующее содержание:
BOOL CDocViewlApp::Initlnstance() {
#ifdef _AFXDLL
Enable3dControls (); #else
Enable3dControlsStatic(); #endif
LoadStdProfileSettings(); CDocTemplate* pDocTemplate; pDocTemplate = new CDocTemplate( 0,
NULL ),
NULL,
NULL );
AddDocTemplate( pDocTemplate ); CMainFrame* pMainFrame = new CMainFrame; pMainFrame->LoadFrame( IDR_RESOURCE ); m_pMainWnd = pMainFrame; pMainFrame->ShowWindow(m_nCmdShow ); pMainFrame->UpdateWindow();
return TRUE;
}
Уважаемый читатель, сейчас нам с вами предстоит разобраться, каким образом должна быть написана простейшая программа работы с MFC. В тех книгах, которые я читал ранее, этот процесс описывался очень отрывочно. Создайте класс, производный от класса CWinApp, перекройте метод lnitlnstance()… А почему так? Почему я должен перекрывать именно метод lnitlnstance(), а не какой-то другой? То есть мне предлагалось просто поверить автору на слово, а я не хочу так делать. Постараемся сейчас самостоятельно разобраться, что к чему.

Если вы помните, уважаемый читатель, я ранее оговорился, что при описании MFC мною был принят за аксиому тот факт, что класс приложения должен быть унаследован от класса CWinApp, включенного в MFC. Что ж, напишем простейшую программу для MFC:
#include <afxwin.h>
class CEmpty : public CWinApp {
};
CEmpty theApp;
Эта функция в настоящее время является устаревшей. В Windows более ранних версий, скажем, Windows 3.1, можно было запустить несколько копий о/уного и того же приложения. При этом весьма высокой была вероятность того, что все они будут использовать одни и те же данные, подготовленные при запуске первой копии приложения. Метод lnitApplication() использовался именно для подготовки ОБЩИХ для всех копий данных при запуске первой копии приложения. В настоящее время этот метод используется для инициализации менеджера документов, который используется при работе с архитектурой «Document/view (документ/представление)». Об этой архитектуре речь пойдет в соответствующей части книги.
Следом за методом lnitApplication() вызывается метод lnitlnstance(). Предназначение этого метода как раз и заключается в том, чтобы произвести инициализацию приложения. Исходный код этого метода находится в файле аррсоге.срр:
BOOL CWinApp::InitInstance() {
return TRUE;
}
Очевидно, что по умолчанию метод не производит никаких действий, т. е. мы должны переопределить этот метод в своей программе и вставить сюда операторы, обеспечивающие реальную инициализацию нашего приложения.
Сразу после завершения работы метода lnitlnstance() вызывается метод Run(). Его исходный код находится в файле аррсоге.срр:
int CWinApp::Run()
{
if (m_pMainWnd == NULL && AfxOleGetUserCtrl()) {
// Not launched /Embedding or /Automation, // but has no main window!
TRACEO("Warning: m_pMainWnd is NULL in CWinApp::Run -
quitting application.\n");
AfxPostQuitMessage(0);
}
return CWinThread::Run();
}
Внимательно взглянув на текст этого метода, можно заметить, что в том случае, когда поле m_pMainWnd равно нулю и приложение было загружено системой, а не OLE, работа метода немедленно завершается. При этом в отладочное окно выдается сообщение «Warning: m_pMainWnd is NULL in CWinApp::Run – quitting application.» (Предупреждение: m_pMainWnd равно NULL в методе CWinApp::Run – приложение завершается). Возникает вопрос: что же мы сделали не так, и почему возникла ошибка? Заглянув в список полей нашего объекта-приложения, мы увидим, что поле m_pMainWnd приложение наследует от класса CWinThread. В описании класса CWinThread можно найти следующую запись:
CWnd* m_pMainWnd; // main window
//(usually same AfxGetApp()->m_pMainWnd)
Другими словами, поле m_pMainWnd должно хранить указатель на объект класса CWnd, который должен быть ассоциирован с окном, являющимся ГЛАВНЫМ окном приложения. Но где должен быть создан этот объект? У нас есть две возможности – метод Ini-tApplicationQ и метод lnitlnstance(). Так как метод InitApplicationQ используется MFC для других целей (инициализация менеджера документов), то, чтобы не переписывать метод lnitApplication(), лучше всего будет создать объект класса CWnd в переопределенном методе lnitlnstance(). Отсюда делаем еще один вывод: объект класса CWnd, который будет ассоциирован с главным окном приложения, целесообразно создавать в переопределенном методе InitlnstanceQ приложения.
А теперь давайте еще немного порассуждаем. Если наши рассуждения верны, то объект класса CWnd будет создан во время отработки метода InitlnstanceQ приложения. Но объект приложения будет создан ранее, во время отработки startup-кода! Инициализацией полей класса, в том числе и поля m_pWinMain, должен, естественно, заниматься конструктор класса. Но конструктор класса CWinThread отрабатывает раньше конструктора класса CWinApp и, тем более, раньше метода lnitlnstance() приложения. Следовательно, поле m_pMainWnd после создания объекта приложения остается неинициализированным или содержит неверную информацию. Значит, после того, как в методе InitlnstanceQ будет создано главное окно приложения, полю m_pMainWnd необходимо присвоить значение указателя на ассоциированный с окном объект. В противном случае программа, увы, работать не будет!
Что ж, попробуем немного изменить нашу программу и добавить в нее переопределенный метод lnitlnstance(), в котором будет создан объект класса CWnd. Текст измененной программы:
#include <afxwin.h>
class CExampleApp : public CWinApp {
BOOL Initlnstance() ;
};
class CMainWnd : public CWnd {
};
BOOL CExampleApp::Initlnstance() {
CMainWnd* pMainWnd = new CMainWnd; m_pMainWnd = pMainWnd; return TRUE;
}
CExampleApp theApp;
Запускаем программу на выполнение… Что такое? Программа «зависла»… Да нет, программа не «зависла». Вспомните, уважаемый читатель, что после метода lnitlnstance() немедленно вызывается метод CWinApp::Run(). Именно в нем программа и «циклит»! Придется опять искать причину неправильной работы программы. Что же может случиться? Мы создали объект класса CWnd, записали указатель на него в поле m_pMainWnd, чего же еще? Взглянув на исходный код метода CWinApp::Run(), можно с уверенностью сказать, что причину «зацикливания» нужно искать в методе CWinThread::Run(). Его исходный текст находится в файле thrdcore.cpp:
int CWinThread::Run() {
ASSERT_VALID(this);
// for tracking the idle time state BOOL bldle = TRUE; LONG HdleCount = 0;
// acquire and dispatch messages until a WM_QUIT message is received, for (;;) {
// phasel: check to see if we can do idle work while (bldle &&
! :
eekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE)) {
// call Onldle while in bldle state
if (!0nldle (HdleCount++) )
bldle = FALSE; // assume "no idle" state
}
// phase2: pump messages while available
do
{
// pump message, but quit on WM_QUIT if (!PumpMessage())
return Exitlnstance ();
// reset "no idle" state after pumping "normal" message if (IsIdleMessage(&m_msgCur)) {
bldle = TRUE; HdleCount = 0;
}
} while (::PeekMessage(&m_msgCur,
NULL, NULL, NULL,
PM_NOREMOVE));
}
ASSERT(FALSE); // not reachable
}
Функции в качестве аргументов передаются стили окон регистрируемого класса, хэндлы курсора, фона и иконки. В отличие от предыдущей эта функция сама формирует имя регистрируемого класса. В том случае, если хэндлы иконки, курсора и фона равны нулю, то имя класса формируется в соответствии с вызовом функции
wsprintf(IpszName,
_T("Afx:%x:%x") , (UINT)hlnst, nClassStyle);
Примером может служить имя Afx:400000:0. Первое число в имени класса обозначает хэндл текущей копии приложения, второе число – стиль окон регистрируемого класса.
Если не все параметры, переданные функции, равны NULL, то формируется полное имя класса в соответствии с функцией
wsprintf(IpszName,
_Т("Afх:%х:%х:%х:%х:%х"),
(UINT)hlnst, nClassStyle,
(UINT)hCursor,
(UINT)hbrBackground,
(UINT)hlcon);
Примером такого имени может служить Afx:400000:8:14c6:0:4687. При этом первое число в имени класса означает хэндл текущей копии приложения, вггорое число – стиль окон регистрируемого класса, третье число означает хэндл курсора, четвертое – хэндл фона, и, наконец, пятое – хэндл иконки окна.
После формирования имени класса функция производит заполнение структуры типа WNDCLASS и регистрирует класс окон, причем в качестве имени класса используется только что сформированное функцией имя.
Зарегистрировав класс окна, можно создавать окно. А создав окно, нужно не забыть его отобразить, верно? Таким образом, мы
«вычислили» те несколько шагов, которые ОБЯЗАТЕЛЬНО нужно выполнить при написании программы с использованием MFC:
1. Описываем класс приложения, производный от класса CWinApp.
2. Описываем класс, к которому будет принадлежать главное окно приложения (обычно CFrameWnd или CMDIFrameWnd).
3. Переопределяем метод lnitlnstance() класса приложения.
4. В методе lnitlnstance() создаем новый объект класса, к которому будет принадлежать главное окно приложения.
5. Указатель на только что созданный объект записываем в поле m__pMainWnd.
6. Регистрируем класс окон, к которому будет принадлежать главное окно приложения.
7. В методе lnitlnstance() создаем непосредственно окно только что зарегистрированного класса, которое будет являться главным окном приложения.
8. Создаем объект класса приложения.
Исходный текст демонстрационной программы, написанной в соответствии с определенной нами последовательностью шагов, приведен ниже:
#include <afxwin.h>
И перед тем, как начать рассказ, о том, что происходит в методе CWinThread::Run(), мне бы хотелось обратить внимание читателя на одну деталь. Раз метод CWinThread::Run() занимается обработкой сообщений, т. е. представляет собой аналог стандартного цикла обработки сообщений, то это означает, что все действия по инициализации нашего приложения, в том числе и создание окна, должны быть осуществлены ДО вызова метода Run()\ Другими словами, MFC написано с таким расчетом, что главное окно приложения будет создано именно в методе lnitlnstance()!
Вернемся к рассмотрению метода CWinThread::Run(). Обращает на себя внимание тот факт, что метод позволяет производить какие-то действия в тот период, когда в очереди нет никаких сообщений. Другими словами, если переменная bldle равна TRUE и функция PeekMessage(), при помощи которой осуществляется выборка сообщений, вернула FALSE, то метод вызывает другой метод, Onldle().
Теперь, понимая процесс создания окна и управления сообщениями, можно начинать писать и более серьезные программы. Отправная точка для этого есть.
Но мы же так и не дошли до конца функции AfxWinMain()! Давайте, завершим эту тему.
Итак, метод Run() в том случае, если в методе lnitlnstance() создано окно, будет работать до тех пор, пока не получит сообщение WM_QUIT. А после того, как метод завершит работу, функция Afx-WinMain() производит освобождение ресурсов при помощи функций AfxLockTempMaps() и AfxUnlockTempMaps(), после чего вызывает функцию AfxWinTerm(), которая и завершает работу функции AfxWinMain().
Выше приведен код, который был сгенерирован компилятором. Этот код находится после входа в метод lnitlnstance(), но до входа в оператор try. Обратим внимание на то, что оператор по смещению 40104А читает что-то по смещению 0 в сегменте, номер которого записан в регистре FS. Но по адресу FS:[0] всегда находится указатель на так называемый псевдорегистр TIB (Thread Information Block). Тип этого TIB’a (извините за невольный каламбур ©) описан в файле winnt.h следующим образом:«
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; union {
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer; struct _NT_TIB *Self; } NT_TIB;
typedef NT_TIB *PNT_TIB;
Нужно ли рпециально говорить о том, что в данном случае нас интересует первое поле этой структуры? Но здесь есть одна тонкость. Дело в том, что тип JEXCEPTION_REGISTRATION-__RECORD не описан ни в заголовочных файлах, ни в исходных файлах библиотеки времени выполнения (CRT), поставляемыми с Microsoft Visual С++. Я предположил (и, судя по всему, предположение оказалось правильным), что тип JEXCEPTION-_REGISTRATION_RECORD – это и что иное, как тип _ЕХСЕР-TION__REGISTRATION, который в файле exsup.inc описан следующим образом:
_EXCEPTION_REGISTRATION struc
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends
