Программирование на языке 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 пре­доставляет только базовые возможности для отображения данных. Надеюсь, у нас еще будет шанс разобраться с этим.

Любознательный читатель уже, наверное, приготовил мне во­прос: да, конечно, все что рассказывается, это верно, но каким же образом создается окно представления и как оно относится к окну фрейма и документу?



На что необходимо обратить внимание? К этому моменту мы еще не представляем характера взаимодействия между докумен­том и фреймом. Поэтому просто предположим, что каким-то обра­зом наш документ будет отображаться в рамках фрейма. Давайте поразмыслим, уважаемый читатель. Мы пишем программу, кото­рая будет работать с многодокументным интерфейсом. Наша про­грамма должна отображать данные в одном из дочерних окон мно­годокументного интерфейса. Следовательно, логично будет в ка­честве фрейма использовать окно класса 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();

}



03.02.2010

Внимательно взглянув на текст этого метода, можно заметить, что в том случае, когда поле m_pMainWnd равно нулю и приложение было загружено системой, а не OLE, работа метода немедленно завершается. При этом в отладочное окно выдается сообщение «Warning: m_pMainWnd is NULL in CWinApp::Run – quitting applica­tion.» (Предупреждение: 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 &&

! : :P 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 Informa­tion 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