// singleDoc.h : interface of the CSingleDoc class // ///////////////////////////////////////////////////////////////////////////// #if !defined(AFX_SINGLEDOC_H__A720367A_E01B_11D1_9525_0080488929D2__INCLUDED_) #define AFX_SINGLEDOC_H__A720367A_E01B_11D1_9525_0080488929D2__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 class CSingleDoc : public CDocument { protected: // create from serialization only CSingleDoc(); DECLARE_DYNCREATE(CSingleDoc) // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CSingleDoc) public: virtual BOOL OnNewDocument(); virtual void Serialize(CArchive& ar); //}}AFX_VIRTUAL // Implementation public: virtual ~CSingleDoc(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: //{{AFX_MSG(CSingleDoc) // NOTE - the ClassWizard will add and remove member functions here. // DO NOT EDIT what you see in these blocks of generated code ! //}}AFX_MSG DECLARE_MESSAGE_MAP() }; ///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line. #endif // !defined(AFX_SINGLEDOC_H__A720367A_E01B_11D1_9525_0080488929D2__INCLUDED_)
// singleView.cpp : implementation of the CSingleView class // #include "stdafx.h" #include "single.h" #include "singleDoc.h" #include "singleView.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CSingleView IMPLEMENT_DYNCREATE(CSingleView, CView) BEGIN_MESSAGE_MAP(CSingleView, CView) //{{AFX_MSG_MAP(CSingleView) // NOTE - the ClassWizard will add and remove mapping macros here. // DO NOT EDIT what you see in these blocks of generated code! //}}AFX_MSG_MAP // Standard printing commands ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview) END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // CSingleView construction/destruction CSingleView::CSingleView() { // TODO: add construction code here } CSingleView::~CSingleView() { } BOOL CSingleView::PreCreateWindow(CREATESTRUCT& cs) { // TODO: Modify the Window class or styles here by modifying // the CREATESTRUCT cs return CView::PreCreateWindow(cs); } ///////////////////////////////////////////////////////////////////////////// // CSingleView drawing void CSingleView::OnDraw(CDC* pDC) { CSingleDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here } ///////////////////////////////////////////////////////////////////////////// // CSingleView printing BOOL CSingleView::OnPreparePrinting(CPrintInfo* pInfo) { // default preparation return DoPreparePrinting(pInfo); } void CSingleView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/) { // TODO: add extra initialization before printing } void CSingleView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/) { // TODO: add cleanup after printing } ///////////////////////////////////////////////////////////////////////////// // CSingleView diagnostics #ifdef _DEBUG void CSingleView::AssertValid() const { CView::AssertValid(); } void CSingleView::Dump(CDumpContext& dc) const { CView::Dump(dc); } CSingleDoc* CSingleView::GetDocument() // non-debug version is inline { ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CSingleDoc))); return (CSingleDoc*)m_pDocument; } #endif //_DEBUG ///////////////////////////////////////////////////////////////////////////// // CSingleView message handlers
// singleView.h : interface of the CSingleView class // ///////////////////////////////////////////////////////////////////////////// #if !defined(AFX_SINGLEVIEW_H__A720367C_E01B_11D1_9525_0080488929D2__INCLUDED_) #define AFX_SINGLEVIEW_H__A720367C_E01B_11D1_9525_0080488929D2__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 class CSingleView : public CView { protected: // create from serialization only CSingleView(); DECLARE_DYNCREATE(CSingleView) // Attributes public: CSingleDoc* GetDocument(); // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CSingleView) public: virtual void OnDraw(CDC* pDC); // overridden to draw this view virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: virtual BOOL OnPreparePrinting(CPrintInfo* pInfo); virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo); virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo); //}}AFX_VIRTUAL // Implementation public: virtual ~CSingleView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: //{{AFX_MSG(CSingleView) // NOTE - the ClassWizard will add and remove member functions here. // DO NOT EDIT what you see in these blocks of generated code ! //}}AFX_MSG DECLARE_MESSAGE_MAP() }; #ifndef _DEBUG // debug version in singleView.cpp inline CSingleDoc* CSingleView::GetDocument() { return (CSingleDoc*)m_pDocument; } #endif ///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line. #endif // !defined(AFX_SINGLEVIEW_H__A720367C_E01B_11D1_9525_0080488929D2__INCLUDED_)
Как правило, многооконные приложения позволяют открыть для одного документа несколько окон просмотра. Чтобы открыть дополнительное окно для просмотра уже открытого документа, нужно выбрать из меню Window строку New Window.
Заголовки окон просмотра одного документа будет одинаковыми за исключением того, что каждое такое окно имеет дополнительный числовой идентификатор, означающий номер окна.
К сожалению, окна просмотра одного документа несогласованны. Если в документ внести изменения через одно окно, они не появятся во втором до тех пор, пока содержимое второго окна не будет перерисовано. Чтобы избежать рассогласования между окнами просмотра одного и того же документа, необходимо сразу после изменения документа в одном окне вызвать метод UpdateAllViews, определенный в классе CDocument:
void UpdateAllViews(CView* pSender, LPARAM lHint=0l, CObject* pHint=NULL);
Метод UpdateAllViews вызывает метод CView::OnUpdate для всех окон просмотра данного документа, за исключением окна просмотра, указанного параметром pSender. Как правило, в качестве pSender используют указатель того окна просмотра, через которое был изменен документ. Его состояние изменять не надо, так как оно отображает последние изменения в документе.
Если изменение документа вызвано другими причинами, не связанными с окнами просмотра, в качестве параметра pSender можно указать значение NULL. В этом случае будут вызваны методы OnUpdate всех окон просмотра без исключения.
Параметры lHint и pHint могут содержать дополнительную информацию об изменении документа. Методы OnUpdate получают значения lHint и pHint и могут использовать их, чтобы сократить перерисовку документа.
Нужно отметить, что содержимое других окон при таком способе использования метода UpdateAllViews перерисовывается полностью, несмотря на то, что дорисовать нужно только одну фигуру. Перерисовка всех объектов может заметно замедлить работу приложения.
Выход из этого положения существует. При вызове метода UpdateAllViews можно указать, какой объект надо дорисовать.
А потом нужно переопределить метод OnUpdate так, чтобы приложение дорисовывало в окнах просмотра только новую фигуру. Для передачи информации от метода UpdateAllViews методу OnUpdate используют параметры lHint и pHint.
Рассмотрим более подробно, как работает метод OnUpdate:
virtual void OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint);
Первый параметр pSender содержит указатель на объект класса окна просмотра, который вызвал изменение документа. Если обновляются все окна просмотра, этот параметр содержит значение NULL.
Второй и третий параметры - lHint и pHint - содержат дополнительную информацию, указанную во время вызова метода UpdateAllViews. Если дополнительная информация не определена, тогда эти параметры содержат соответственно значения 0l и NULL.
Метод OnUpdate вызывается, когда после изменения документа вызывается метод CDocument::UpdateAllViews. Метод OnUpdate также вызывается методом OnInitialUpdate (если он не переопределен в приложении).
Реализация OnUpdate из класса CView определяет, что внутренняя область окна просмотра подлежит обновлению и передает данному окну сообщение WM_PAINT (для этого вызывается функция InvalidateRect). Это сообщение обрабатывается методом OnDraw.
Параметр lHint имеет тип LPARAM и может содержать любое 32-битное значение. Параметр pHint является указателем на объект типа CObject. Поэтому, если есть необходимость его использовать, нужно определить собственный класс, наследованный от базового класса CObject, создать объект этого класса и передать указатель на него методу UpdateAllViews.
Указатель на объект класса, наследованного от CObject, можно присвоить указателю на объект класса CObject, поэтому такая операция разрешена. Следовательно, через этот указатель можно передавать объекты различных типов, наследованных от CObject.
При разработке метода OnUpdate необходимо проверять тип объекта, передаваемого через параметр pHint. Для этого можно воспользоваться методом IsKindOf класса CObject. Метод IsKindOf позволяет узнать тип объекта уже на этапе выполнения приложения.
Когда переопределяется метод OnUpdate, необходимо иметь в виду, что этот метод вызывается не только методом UpdateAllViews. В некоторых случаях он может быть вызван, если надо перерисовать все изображение в окне просмотра. В этом случае параметр lHint содержит 0, а параметр pHint - NULL. Необходимо обрабатывать эту ситуацию отдельно, вызывая метод CView::OnUpdate и обновляя все окно целиком.
Иногда при работе с несколькими потоками или процессами появляется необходимость синхронизировать выполнение двух или более из них. Причина этого чаще всего заключается в том, что два или более потоков могут требовать доступ к разделяемому ресурсу, который реально не может быть предоставлен сразу нескольким потокам. Разделяемым называется ресурс, доступ к которому могут одновременно получать несколько выполняющихся задач.
Механизм, обеспечивающий процесс синхронизации, называется ограничением доступа. Необходимость в нем возникает также в тех случаях, когда один поток ожидает события, генерируемого другим потоком. Естественно, должен существовать какой-то способ, с помощью которого первой поток будет приостановлен до совершения события. После этого поток должен продолжить свое выполнение.
Имеется два общих состояния, в которых может находиться задача. Во-первых, задача может выполняться (или быть готовой к выполнению, как только получит доступ к ресурсам процессора). Во-вторых, задача может быть блокирована. В этом случае ее выполнение приостановлено до тех пор, пока не освободится нужный ей ресурс или не произойдет определенное событие.
В Windows имеется специальные сервисы, которые позволяют определенным образом ограничить доступ к разделяемым ресурсам, ведь без помощи операционной системы отдельный процесс или поток не может сам определить, имеет ли он единоличный доступ к ресурсу. Операционная система Windows содержит процедуру, которая в течении одной непрерывной операции проверяет и, если это возможно, устанавливает флаг доступа к ресурсу. На языке разработчиков операционной системы такая операция называется операцией проверки и установки. Флаги, используемые для обеспечения синхронизации и управления доступом к ресурсам, называются семафорами (semaphore). Интерфейс Win32 API обеспечивает поддержку семафоров и других объектов синхронизации. Библиотека MFC также включает поддержку данных объектов.
Вывод на графическое устройство привязан к некоторой системе координат. Практически все методы, обеспечивающие вывод на графическое устройство, привязаны к логической (или оконной) системе координат. В этой системе координаты называют логическими.
Свойства этой системы можно менять. Описание логической системы координат задается контекстом устройства. Чтобы логические координаты могли интерпретироваться физическим устройством, используется следующий механизм. Графическое устройство имеет свою собственную фиксированную физическую систему координат. Контекст устройства содержит атрибуты, задающие соответствие между логическими и физическими координатами точки, что позволяет при выводе на графическое устройство произвести преобразование логических координат в физические. Графическое устройство осуществляет вывод в прямоугольную область отображения (viewport). Начало системы координат находится в левом верхнем углу области, первая координатная ось (Х) направлена горизонтально слева направо, вторая - Y вертикально сверху вниз. Единицей измерения является пиксел.
При выводе на дисплей в системе Windows существует как бы три графических устройства, зависящих от типа экранной области, в которую осуществляется вывод. Вывод может производиться:
на экран, и тогда область отображения - весь экран;
в Windows-окно, и тогда область отображения - все Windows-окно;
в клиентскую часть Windows-окна, и тогда область отображения - клиентская часть Windows-окно.
Для каждого из этих графических устройств можно создать свой тип контекста устройства.
Тип логической системы координат (mapping mode) является атрибутом контекста устройства. Соответствие между системой координат области отображения и логической системой координат окна задается атрибутами:
выделенная точка окна (window origin);
выделенная точка области отображения (viewport origin);
меры протяженности логической (оконной) системы (window extent);
меры протяженности области отображения (viewport extent).
Используя эти атрибуты, Windows преобразует логические координаты в физические и выводит на графическое устройство.
Тип системы координат | Единица измерения | Направление оси X | Направление оси Y |
MM_TEXT | Пиксел | Координата увеличивается слева направо | Координата увеличивается сверху вниз |
MM_LOMETRIC | 0.1 мм | Координата увеличивается слева направо | Координата увеличивается снизу вверх |
MM_HIMETRIC | 0.01 мм | Координата увеличивается слева направо | Координата увеличивается снизу вверх |
MM_LOENGLISH | 0.01 дюйма | Координата увеличивается слева направо | Координата увеличивается снизу вверх |
MM_HIENGLISH | 0.001 дюйма | Координата увеличивается слева направо | Координата увеличивается снизу вверх |
MM_TWIPS | 1/1440 дюйма | Координата увеличивается слева направо | Координата увеличивается снизу вверх |
MM_ISOTROPIC | Усл.единицы (по X и У совпадают) | устанавливается | Устанавливается |
MM_ANISOTROPIC | Усл.единицы (по X и Y задаются независимо) | устанавливается | Устанавливается |
При использовании собственных библиотек или библиотек независимых разработчиков придется обратить внимание на согласование вызова функции с ее прототипом.
Если бы мир был совершенен, то программистам не пришлось бы беспокоиться о согласовании интерфейсов функций при подключении библиотек — все они были бы одинаковыми. Однако мир далек от совершенства, и многие большие программы написаны с помощью различных библиотек без C++.
По умолчанию в Visual C++ интерфейсы функций согласуются по правилам C++. Это значит, что параметры заносятся в стек справа налево, вызывающая программа отвечает за их удаление из стека при выходе из функции и расширении ее имени. Расширение имен (name mangling) позволяет редактору связей различать перегруженные функции, т.е. функции с одинаковыми именами, но разными списками аргументов. Однако в старой библиотеке С функции с расширенными именами отсутствуют.
Хотя все остальные правила вызова функции в С идентичны правилам вызова функции в C++, в библиотеках С имена функций не расширяются. К ним только добавляется впереди символ подчеркивания (_).
Если необходимо подключить библиотеку на С к приложению на C++, все функции из этой библиотеки придется объявить как внешние в формате С:
extern "С" int MyOldCFunction(int myParam);
Объявления функций библиотеки обычно помещаются в файле заголовка этой библиотеки, хотя заголовки большинства библиотек С не рассчитаны на применение в проектах на C++. В этом случае необходимо создать копию файла заголовка и включить в нее модификатор extern "C" к объявлению всех используемых функций библиотеки. Модификатор extern "C" можно применить и к целому блоку, к которому с помощью директивы #tinclude подключен файл старого заголовка С. Таким образом, вместо модификации каждой функции в отдельности можно обойтись всего тремя строками:
extern "С" { #include "MyCLib.h" }
В программах для старых версий Windows использовались также соглашения о вызове функций языка PASCAL для функций Windows API. В новых программах следует использовать модификатор winapi, преобразуемый в _stdcall. Хотя это и не стандартный интерфейс функций С или C++, но именно он используется для обращений к функциям Windows API. Однако обычно все это уже учтено в стандартных заголовках Windows.
Чтобы приложение имело возможность сохранения документов в файле, нужно изменить метод Serialize класса документа. Метод Serialize вызывается всякий раз, когда надо сохранить документ в файле на диске или загрузить его из существующего файла. В частности, этот метод вызывается, когда пользователь выбирает из меню File строки Save, Save As и Open.
Средство MFC AppWizard подготавливает шаблон метода Serialize для класса, представляющего документ приложения:
void CDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: add storing code here } else { // TODO: add loading code here } }
В методе Serialize необходимо определить, как он должен сохранять и восстанавливать документы приложения.
Метод Serialize имеет единственный параметр ar, представляющий ссылку на объект класса CArchive. Этот объект, называемый архивом, представляет файл документа, расположенный на диске. Кроме того, архив несет в себе информацию о том, что делать с документом - записать его в файл или выгрузить из файла.
Для того, чтобы определить, какую операцию надо выполнить, используется метод IsStoring, определенный в классе CArchive. Если этот метод возвращает ненулевое значение для объекта ar, переданного методу Serialize, значит, надо сохранить документ в файле.
Перед открытием приложением другого файла-документа (или перед созданием нового) при наличии внесенных изменений в текущий документ принято предупреждать об этом пользователя. Класс CDocument и все классы, для которых он является базовым, позволяют установить специальный флаг модификации, означающий, что документ был изменен. В этом случае перед закрытием документа пользователю будет предложено его сохранить. Для установки этого флага предназначен метод SetModifiedFlag.
Если документ изменен, необходимо установить флаг модификации, вызвав метод SetModifiedFlag с параметром bModified, равным TRUE, или без параметра. В случае необходимости можно убрать установленный флаг. Для этого надо вызвать метод SetModifiedFlag с параметром bModified, равным FALSE. Устанавливать флаг модификации нужно в методах, выполняющих модификацию документа.
Чтобы приложение имело возможность сохранения документов в файле, нужно изменить метод Serialize класса документа. Метод Serialize вызывается всякий раз, когда надо сохранить документ в файле на диске или загрузить его из существующего файла. В методе Serialize необходимо определить, как он должен сохранять и восстанавливать документы приложения.
Использование стандартных диалогов выбора файлов
Для того, чтобы иметь возможность при открытии и сохранения документа на диске использовать стандартные диалоговые панели выбора файла с определенными программистом настройками, необходимо в приложении обработать командные сообщения с идентификаторами ID_FILE_OPEN, ID_FILE_SAVE, ID_FILE_SAVE_AS (следует отметить, что обработка этих сообщений должна происходить в разных классах приложения – см. ниже).
В класс документа необходимо добавить элемент BOOL status_new, принимающий значение TRUE, если пользователь работает с новым документом, и значение FALSE, если пользователь открыл документ, содержащийся в файле на диске. В методе OnNewDocument класса документа необходимо присвоить элементу status_new значение TRUE.
При помощи средства ClassWizard добавить в главный класс приложения (наследованный от класса CWinApp) обработчик командного сообщения с идентификатором ID_FILE_OPEN. Используемый ранее по умолчанию обработчик CWinApp::OnFileOpen этого сообщения убрать из списка обрабатываемых сообщений:
BEGIN_MESSAGE_MAP(CApp, CWinApp) …… ON_COMMAND(ID_FILE_OPEN, OnFileOpen) …… END_MESSAGE_MAP()
Изменить заготовку метода OnFileOpen главного класса приложения следующим образом:
void CApp::OnFileOpen() { // Создание стандартной панели выбора файла "Open" CFileDialog DlgOpen(TRUE,(LPCTSTR)"txt",NULL, OFN_HIDEREADONLY,(LPCSTR)" Text Files (*.txt) |*.txt");
// Отображение стандартной панели выбора файла "Open" if(DlgOpen.DoModal()==IDOK) { // Создание объекта класса документа, связанного с // файлом, и его окна просмотра. // OpenDocumentFile - метод класса CWinApp CDocument *pDoc =OpenDocumentFile(DlgOpen.GetPathName());
За последние 35 лет конструкторы аппаратного обеспечения прошли путь от компьютеров размеров с комнату до маленьких "лэптопов" на основе крошечных, но мощных микропроцессоров. За те же самые 35 лет разработчики программ прошли путь от больших систем на ассемблере и COBOL до создания еще больших систем на С и C++. Это (вероятно) прогресс, но мир программного обеспечения все же развивается медленнее, чем мир “железа”. Что же есть такого у конструкторов аппаратного обеспечения, чего нет у конструкторов программ?
Компоненты — вот что. Проектировщик аппаратуры обычно создает систему из уже готовых компонентов, каждый из которых выполняет определенную функцию и предоставляет набор сервисов через тщательно определенные интерфейсы. Задача проектировщиков аппаратуры значительно упрощается, благодаря повторному использованию результатов, достигнутых другими.
Повторное применение — это также путь к созданию лучшего программного обеспечения. Сегодня разработчики программ частенько повторяют дорогу, уже проторенную до них сотнями программистов. Результат часто бывает очень хорошим, но ведь мог бы быть гораздо лучше. Создание новых приложений из существующих, протестированных компонентов, вероятно, должно приводить к более надежному коду. И — что не менее важно — оно может быть гораздо быстрее и дешевле.
Именно этот подход и реализуется в СОМ. Объекты СОМ являются эффективным механизмом применения повторного программного обеспечения, так как позволяют создавать дискретные, повторно используемые компоненты. Эти компоненты, каждый из которых предназначен для выполнения определенной функции, могут играть роль, аналогичную той, что играют различные микросхемы, используемые проектировщиками аппаратуры. Возможно, именно из-за подобной аналогии этот подход стал известен как компонентное программное обеспечение.
Идею трудно назвать новой. Существующие механизмы повторного применения, хотя и важны сами по себе, но не слишком мощны. Чтобы разобраться в этом, рассмотрим две наиболее распространенные схемы повторного применения: библиотеки и объекты.
В традиционном простом процессе в один и тот же момент времени выполнялось только одно действие. Другими словами, у процесса был только один поток управления (thread of execution). Однако иногда полезно обеспечить выполнение процессом нескольких задач одновременно (или хотя бы чтобы они казались выполняющимися одновременно). С этой целью у процесса может быть более одного потока управления, т.е. он становится многопоточным (multithreaded). Многопоточный процесс может повысить производительность, например, в тех случаях, когда в компьютере установлено несколько процессоров и процесс может назначать потоки на разные процессы. Многопоточность может также пригодиться в распределенной среде, где процесс одной машине выполняет запрос к другой. Вместо того, чтобы пассивно ждать, пока вторая машина отработает запрос, вызывающий процесс может использовать отдельный поток для выполнения полезной работы, пока запрос не будет выполнен. Многопоточность вносит дополнительные сложности и в программирование. Теперь программист должен учитывать возможность возникновения конфликтов внутри процесса, когда, например, два потока пытаются изменять одну переменную. Корректная обработка таких ситуаций требует дополнительных усилий. Библиотеки, используемые многопоточными программами, также должны быть многопоточными, иначе могут возникнуть странные и трудные для локализации ошибки. (Одна из причин сложности локализации таких ошибок в том, что их трудно воспроизвести. Так как детали выполнения потока могут изменяться от одного запуска программы к другому, точные обстоятельства проявления ошибки могут возникать лишь от случая к случаю.)
На некоторых платформах, где СОМ использовалась первоначально, — Microsoft Windows 3.x и Macintosh — вопрос потоков не возникает. Так как ни одна из этих операционных систем не поддерживает потоки, то и опасности, связанные с ними, отсутствуют. Но Microsoft Windows NT и Microsoft Windows 95, как и другие платформы, поддерживающие СОМ, допускают создание многопоточных процессов, поэтому для эффективного использования СОМ в таких средах учитывать вопросы, связанные с потоками, необходимо.
Объекты — центральная идея СОМ. Но определение и использование объектов здесь иногда отличается от других популярных объектных технологий. Чтобы понять соотношение СОМ и других объектно-ориентированных технологий, следует разобраться, что обычно стоит за термином “объектно-ориентированный” и как СОМ этому соответствует.
Определение объекта
Объектно-ориентированная технология должна характеризоваться несколькими ключевыми понятиями. И главным среди них является общепризнанное определение того, что составляет объект. Принято считать, что объект состоит из двух элементов: предопределенного набора данных (его также называют состоянием или атрибутами) и группы методов. Последние, реализуемые обычно в виде процедур или функций, позволяют клиенту запросить у объекта выполнение определенных задач.
До этого места СОМ-объекты именно таковы. Но в большинстве объектных технологий объект поддерживает только один интерфейс с одним набором методов. А вот СОМ-объекты могут — и почти всегда это делают — поддерживать более одного интерфейса. Например, у С++-объекта лишь один интерфейс, включающий в себя все методы объекта. СОМ-объект с его несколькими интерфейсами может быть отлично реализован с несколькими объектами C++ — по одному на каждый интерфейс СОМ-объекта (хотя C++ — не единственный язык, который можно использовать для создания СОМ-объектов; cледует отметить, что, подобно объектам СОМ, объекты языка программирования Java также могут иметь несколько интерфейсов, фактически, Java отлично подходит и для создания СОМ-объектов другими способами.).
Еще одна распространенная концепция в объектно-ориентированной технологии — понятие класса. Скажем, все объекты, представляющие банковские счета, можно отнести к одному классу. Любой конкретный объект “банковский счет”, например, представляющий счет, является экземпляром данного класса.
СОМ-объекты имеют и классы. Класс в СОМ понимается как конкретная реализация набора интерфейсов. Может существовать несколько разных реализации одного и того же набора интерфейсов, каждая из которых будет отдельным классом.
С точки зрения клиента, только интерфейсы имеют значение. Клиента не касается реализация интерфейсов, т.е. то, что фактически определяет класс. Возможность работать с объектами разных типов, каждый из которых поддерживает данный набор интерфейсов, но реализует их по-разному, называется полиморфизмом.
Инкапсуляция, полиморфизм и наследование
Чтобы считать технологию объектно-ориентированной, достаточно ли того, что она, моделируя предметы как группы методов и данных, затем организует эти группы в классы? В общем случае объектная ориентированность требует поддержки еще трех характеристик: инкапсуляции, наследования и полиморфизма.
Инкапсуляция означает, что данные объекта недоступны его клиентам непосредственно. Вместо этого они инкапсулируются — скрываются от прямого доступа извне. Единственный способ доступа к данным объекта — его методы. В совокупности они представляют предопределенный интерфейс с внешним миром, и пользователь объекта может считывать или модифицировать данные последнего только через этот интерфейс. Инкапсуляция предохраняет данные объекта от нежелательного доступа, позволяя объекту самому управлять доступом к своим данным. Предотвращая случайные, некорректные изменения данных объекта, инкапсуляция может оказать значительную помощь в создании более качественных программ.
C++ предоставляет непосредственную поддержку инкапсуляции (впрочем, и способы ее обойти). В случае некорректной попытки непосредственно модифицировать данные объекта компилятор может выдать программисту сообщение об ошибке. Хотя СОМ и не является языком программирования, идея остается той же самой. Клиент имеет доступ к данным объекта СОМ только через методы интерфейсов этого объекта. Данные объекта СОМ инкапсулированы.
Второй определяющей характеристикой объектно-ориентированных технологий является полиморфизм. Полиморфизм, попросту говоря, означает, что клиент может рассматривать разные объекты как одинаковые и каждый из объектов будет вести себя соответствующим образом. Возьмем, к примеру, объект, представляющий расчетный счет.
У него, вероятно, имеется метод Withdrawal, которые неявно вызывается всякий раз при выписки чека. Также может быть объект, представляющий сберегательный счет, также обладющий методом Withdrawal. Для клиента оба метода выглядят одинаково; и при вызове любого из них происходит то же самое: остаток на счете в объекте уменьшается.
Однако фактическая реализация этих двух методов может быть абсолютно разной. Реализация для сберегательного счета может просто сравнить снимаемую сумму с остатком на счете. Если изъятие меньше остатка, операция выполняется, в противном случае — нет. С другой стороны, метод Withdrawal для расчетного счета может быть сложнее. Обычно расчетные счета допускают некоторый объем автоматического кредитования, если сумма чека превышает остаток. Реализация метода Withdrawal для объекта — расчетного счета могла бы сравнивать сумму чека как с текущим остатком счета, так и с максимально допустимым размером кредита. В данном случае запрос отрабатывается успешно, и чек принимается к оплате, если его сумма меньше суммы текущего остатка и максимального размера кредита.
С точки зрения клиента, оба метода Withdrawal одинаковы; различия в реализации — причем, существенные — скрыты. Возможность рассматривать разные вещи единообразно, хотя каждая ведет себя по-своему, — суть полиморфизма. Приведенный пример также демонстрирует огромную пользу полиморфизма: клиенты могут оставаться в блаженном неведении относительно вопросов, которые их не касаются, что упрощает разработку клиентского программного обеспечения.
Эту идею реализуют в полной мере СОМ-объекты. Объекты двух разных классов вполне могут предоставлять своим клиентам одинаковый набор интерфейсов или, может быть, только одно общее определение метода, даже если каждый объект реализует соответствующие методы по-своему.
Последняя определяющая характеристика традиционных объектно-ориентированных технологий — наследование. Идея проста: имея некоторый объект, можно создать новый, автоматически поддерживающий все или некоторые “способности” старого.
Подобно тому, как человек без каких-либо усилий со своей стороны может унаследовать от своих родителей какие-либо признаки, так и объект может автоматически получать характеристики другого объекта.
Виды наследования бывают разные. Следует отметить различия наследования реализации и наследования интерфейса. В первом случае объект наследует от своего родителя код. Когда клиент дочернего объекта вызывает один из унаследованных методов, на самом деле выполняется код метода родителя. Однако в случае наследования интерфейса потомок наследует только определения методов родителя. При вызове клиентом потомка одного из этих методов последний должен самостоятельно предоставить код обработки запроса.
Наследование реализации — это механизм повторного использования кода, широко применяемый в языках типа C++ и Smalltalk. В противоположность этому наследование интерфейса в действительности означает повторное использование спецификации — определений методов, поддерживаемых объектом. Наследования интерфейса облегчает к тому же реализацию полиморфизма. Определение нового интерфейса путем наследования от существующего гарантирует, что объект, поддерживающий новый интерфейс, можно рассматривать как объект, который поддерживает старый интерфейс.
Языки программирования типа C++ и Smalltalk поддерживают как наследование реализации, так и наследование интерфейса. Однако СОМ-объекты поддерживают только наследование интерфейса. Создатели СОМ полагали, что с учетом ее универсальности наследование реализации будет неприемлемым (и даже потенциальной опасным) способом повторного использования одного СОМ-объекта другим. Например, поскольку наследование реализации часто открывает наследующему объекту детали реализации родителя, постольку это может нарушить инкапсуляцию последнего. Поддержка только наследования интерфейса, что имеет место в СОМ, позволяет использовать повторно ключевой элемент другого объекта — его интерфейс — и в то же время избежать указанной выше проблемы.
Но как СОМ-объект повторно использует код другого объекта в отсутствие наследования реализации? Для этого в СОМ имеются механизмы включения (containment) и агрегирования (aggregation). При включении один объект просто вызывает другой по мере надобности для выполнения своих функций.
При агрегировании объект представляет один или несколько интерфейсов другого объекта как свои собственные; то, что клиент видит как один объект, предоставляющий группу интерфейсов, на самом деле — два или несколько объектов, агрегированных вместе. Как можно ожидать, реализация агрегирования требует несколько больших усилий, чем включение, но оба механизма — эффективные способы создания надстройки над существующими СОМ-объектами.
Является ли СОМ по-настоящему объектно-ориентированной?
У СОМ много общего с другими объектно-ориентированным технологиями. В ее основе лежит понятие объекта как набора данных и методов, схожее с идеей такого языка, как C++, хотя СОМ и позволяет одному объекту иметь несколько интерфейсов. СОМ также обеспечивает инкапсуляцию, полиморфизм и наследование интерфейсов, однако повторное использование кода в ней осуществляется через включение и агрегирование, а не посредством наследования реализации. Основой СОМ являются объекты, однако способ определения и их поведение несколько отличаются от других распространенных объектно-ориентированных технологий.
Итак, является ли СОМ по-настоящему объектно-ориентированной? Смотря что имеется в виду. Если спрашивается: "Являются ли объекты СОМ в точности такими же, как объекты в языках типа C++? " — ответ однозначно отрицательный. Это не должно слишком удивлять, так как СОМ предназначена для решения совершенно иных проблем, нежели объектно-ориентированные языки программирования. Но если вопрос стоит так: "Предоставляет ли СОМ основные возможности и преимущества объектов? " — то ответ столь же очевидно положительный, и только такая постановка проблемы имеет значение.
Интернет и стиль доступа к данным, обеспечиваемый WWW, обрушились на берега информатики, как цунами. Microsoft не первая осознала влияние, которое может оказать эта волна, но не замедлила с ответом. Не удивительно, что большинство новых технологий Microsoft в данной области созданы с использованием СОМ. Уже говорилось, что ActiveX своим названием обязана встрече СОМ и Интернета, хотя теперь оно и распространилось на многие другие технологии на основе СОМ.
Компонентный подход СОМ находит различные применения в технологиях Microsoft для Интернета и WWW. Например, средство просмотра WWW фирмы Microsoft — Проводник по Интернету (Internet Explorer) — активно использует расширение технологии составных документов OLE — Документы ActiveX (ActiveX Documents). Благодаря этому расширению, пользователь может просматривать информацию разного типа в дополнение к обычным страницам HTML (Hypertext Markup Language — Язык разметки гипертекста). Технология управляющих элементов ActiveX была расширена, чтобы код и данные управляющих элементов могли при необходимости загружаться с сервера WWW и исполняться внутри средства просмотра. Сценарии ActiveX (ActiveX Scripting) — универсальный способ исполнения клиентами сценариев, написанных на любом языке, тогда как технология Гиперсвязей ActiveX (ActiveX Hyper-links), в основе которой лежат моникеры, обеспечивает создание гиперсвязей в стиле WWW на только между страницами HTML, но и между любыми типами документов.
Приложения с каждым днем становятся все сложнее. В текстовые процессоры добавляются графические возможности, в электронные таблицы средства построения диаграмм, и кажется, все кончится одним большим приложением для решения всех задач. Но в действительности цель как раз не в этом, а в интеграции разных приложений. Например, добавлять поддержку графики в текстовый процессор не потребуется, если внутри него можно будет использовать некоторое уже существующее графическое приложение. Задача — обеспечить гладкую совместную работу приложений. Пользователю должно представляться нечто такое, что выглядит как один документ, хотя на самом деле над разными частями такого документа совместно работают разные приложения.
Для решения этой проблемы предназначена технология OLE (ранее называвшаяся Документы OLE — OLE Documents). Поддерживая нужные СОМ-объекты, каждый с собственным набором интерфейсов, независимые приложения могут совместно работать, чтобы пользователь получил один составной документ. Все эти интерфейсы носят абсолютно общий характер — ни одно приложение не знает, что представляют собой другие. Зачем встраивать в текстовый процессор функции электронной таблицы? OLE поможет просто задействовать в случае необходимости существующее приложение электронной таблицы.
Определенный OLE стандартный интерфейс обеспечивает взаимодействие между приложениями любых типов и любых производителей, а не только между электронными таблицами и текстовыми процессорами Microsoft.
При создании составного документа с помощью OLE одно из приложений всегда является контейнером. Как следует из названия, контейнер определяет самый общий документ, в котором содержится все остальное. Другие приложения — серверы — могут размещать свои документы внутри документа-контейнера.
При использовании OLE документ сервера может быть либо связан, либо внедрен в документ контейнера. Связанный документ сервера хранится в отдельном файле, а в документе контейнера хранится лишь связь с этим файлом. (На самом деле связью является моникер.) Внедренный документ сервера хранится в том же файле, что и документ контейнера. (Два приложения при этом совместно используют общий файл с помощью структурированного хранилища.)
Как и в случае разработки диалоговых панелей, сначала следует подготовить шаблон диалоговой панели, которая будет использоваться как основа панели управления. В шаблоне диалоговой панели следует установить только один стиль WS_CHILD - диалоговая панель будет выступать в качестве дочернего окна и не должна иметь ни заголовка, ни рамки. Готовый шаблон следует сохранить в файле ресурсов, например, с идентификатором ресурса IDD_DIALOGBAR.
В состав класса окна, в котором будет отображаться диалоговая панель управления, надо включить элемент данных класса CDialogBar. Этот элемент будет представлять диалоговую панель управления. Например:
class CMainFrame : public CMDIFrameWnd { protected: CDialogBar m_wndDlgBar; // диалоговая панель управления // другие описания класса ....... };
Затем необходимо переопределить метод OnCreate класса окна и добавить в него вызов метода Create класса CDialogBar для объекта, представляющего диалоговую панель управления.
Для того, чтобы пользователь мог сам перемещать диалоговую панель управления с одной границы экрана на другую или разместить ее в отдельном окне, т.е. чтобы он мог управлять положением панели на экране, необходимо сделать действия, аналогичные действиям для панели управления класса CToolBar.
Обычно создание диалоговой панели управления и разрешение ее перемещения производят при обработке сообщения WM_CREATE для главного окна приложения, например:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { // вызов метода базового класса для корректного создания окна if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1) return -1;
// разрешить присоединение панелей ко всем сторонам окна EnableDocking(CBRS_ALIGN_ANY);
// создание диалоговой панели управления if(!m_wndDlgBar.Create(this,IDD_DIALOGBAR, CBRS_BOTTOM,IDD_DIALOGBAR)) return -1;
// установить характеристики диалоговой панели управления m_wndDlgBar.SetBarStyle(m_wndDlgBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);
// разрешить присоединить панель к любой строке родительского окна m_wndDlgBar.EnableDocking(CBRS_ALIGN_ANY);
// присоединить диалоговую панель управления к родительскому окну DockControlBar(&m_wndDlgBar);
// создание панели управления (процесс создания рассматривался выше) ……… // создание панели состояния (процесс создания рассматривается ниже) ……… return 0; }
Элементы управления диалоговой панели передают сообщения непосредственно своему родительскому окну. Поэтому следует добавить в таблицу класса окна соответствующие макрокоманды для получения сообщений и включить в класс окна методы для обработки этих сообщений.
Если диалоговая панель управления достаточно сложна и имеет много органов управления, то можно создать для нее отдельный класс на основе базового класса CDialogBar (однако это способ не очень часто используется). Этот класс следует дополнить методами, обрабатывающими сообщения от элементов управления. Таким образом можно разгрузить таблицу сообщений главного окна приложения.
Теперь, познакомившись с принципами работы библиотек DLL в приложениях, рассмотрим способы их создания. При разработке приложении функции, к которым обращается несколько процессов, желательно размещать в DLL. Это позволяет более рационально использовать память в Windows.
Проще всего создать новый проект DLL с помощью мастера AppWizard, который автоматически выполняет многие операции. Для простых DLL, таких как рассмотренные в этой главе, необходимо выбрать тип проекта Win32 Dynamic-Link Library. Новому проекту будут присвоены все необходимые параметры для создания библиотеки DLL. Файлы исходных текстов придется добавлять к проекту вручную.
Если же планируется в полной мере использовать функциональные возможности MFC, такие как документы и представления, или намерены создать сервер автоматизации OLE, лучше выбрать тип проекта MFC AppWizard (dll). В этом случае, помимо присвоения проекту параметров для подключения динамических библиотек, мастер проделает некоторую дополнительную работу. В проект будут добавлены необходимые ссылки на библиотеки MFC и файлы исходных текстов, содержащие описание и реализацию в библиотеке DLL объекта класса приложения, производного от CWinApp.
Иногда удобно сначала создать проект типа MFC AppWizard (dll) в качестве тестового приложения, а затем — библиотеку DLL в виде его составной части. В результате DLL в случае необходимости будет создаваться автоматически.
Так как документы и их облики являются одними из наиболее важных составляющих архитектуры Document-View, их создание выполняется каркасом приложения. Процесс создания довольно сложен, остановимся на наиболее существенных моментах.
Первый связан со специальными объектами - документными шаблонами (document template). Принадлежа классу CDocTemplate (класс "Документный шаблон") или производным от него классам, они задают описание наиболее существенной части архитектуры, включая описание наиболее существенной части архитектуры Document-View, включая описание класса документа, класса облика документа и класса окна-рамки документа. Документные шаблоны бывают двух видов: для однодокументного интерфейса (SDI) и для многодокументного интерфейса (MDI).
Создание документа и связанных с ним объектов выполняется при открытии пользователем документа - существующего или нового. Это происходит в начале работы приложения, а также когда пользователь выбирает команды Open и New в меню File.
Создание начинается с выбора шаблона документа. В соответствии с описанным в шаблоне классом документа создается сам документ. Если шаблон задает MDI-интерфейс, то каждый раз создается новый объект-документ. Если же шаблон задает SDI-интерфейс, новый объект создается только один раз, а при выборе пользователем команд Open и New производится только "обнуление" имеющегося объекта документа.
Для инициализации созданного документа каркас приложения вызывает метод OnNewDocument при открытии нового документа, а при открытии уже имеющегося - OnOpenDocument. Оба метода принадлежат базовому классу CDocument. Они являются виртуальными, и поэтому, переопределив их, программист может задать необходимые действия по инициализации документа. Однако, эти методы и сами по себе выполняют ряд действий, связанных с инициализацией, - в частности, проверяют, что объект-документ пуст. Переопределенный метод заменяет метод базового класса, поэтому нужно предусмотреть вызов в нем метода базового класса. Остовы методов OnNewDocument и OnOpenDocument, созданные AppWizard, содержат такой вызов.
CWnd();
Создает объект класса CWnd, обеспечивающий доступ к Windows-окну. При этом само Windows-окно не создается. Далее можно либо создать новое Windows-окно и закрепить его за данным оконным объектом, либо закрепить уже имеющееся Windows-окно. Первое достигается методами CreateEx и Create, второе - методом Attach.
virtual BOOL DestroyWindow();
Уничтожает Windows-окно, закрепленное за объектом класса CWnd. Если окно уничтожено успешно, возвращается ненулевое значение, в противном случае - 0. После выполнения этого метода оконный объект уже не имеет закрепленного за ним Windows-окна. Сам оконный объект при этом не уничтожается. Метод DestroyWindow посылает соответствующие сообщения, чтобы уничтожить окно и связанные с ним ресурсы. Оно также уничтожает дочерние окна и, если требуется, информирует родительское окно.
Библиотека классов MFC определяет механизм записи и восстановления объектов (serialization), причем поддержка этого механизма осуществляется средствами класса CObject.
Классы, наследованные от CObject, также могут обеспечивать работу механизма записи и восстановления объектов. Для этого при объявлении класса надо указать макрокоманду DECLARE_SERIAL, а при определении - макрокоманду IMPLEMENT_SERIAL.
Макрокоманду DECLARE_SERIAL необходимо поместить в описании класса во включаемом файле. Непосредственно после этой макрокоманды надо указать имя класса
DECLARE_SERIAL (имя_класса)
Макрокоманду IMPLEMENT_SERIAL следует указать перед определением класса в файле исходного текста приложения. Прототип макрокоманды IMPLEMENT_SERIAL представлен ниже:
IMPLEMENT_SERIAL (имя_класса, имя_базового_класса, номер_версии)
Параметр имя_класса определяет имя класса, имя_базового_класса - имя базового класса, из которого непосредственно наследуется класс. Последний параметр номер_версии - это число типа UINT, определяющее версию программы. Если разрабатывается новая версия приложения и изменяется набор данных, которые необходимо записать в файл, нужно изменить значение параметра номер_версии.
В классе должны быть определены специальные методы для записи и восстановления состояния объектов этого класса. Обычно эти методы сохраняют и восстанавливают элементы данных из класса. Таким образом, объекты класса сами отвечают за то, как они сохраняют и восстанавливают свое состояние.
Методы, сохраняющие и восстанавливающие объектов, взаимодействуют с объектом класса CArchive, который осуществляет непосредственную запись и чтение информации из файла на диске.
Класс CObject содержит виртуальный метод Serialize, отвечающий за запись и чтение объектов классов, наследованных от класса CObject:
virtual void Serialize(CArchive& ar);
В качестве параметра ar методу передается указатель на объект класса CArchive, используемый для записи и восстановления состояния объекта класса CObject (или наследуемого от него класса).
Чтобы узнать, какую операцию должен выполнить метод Serialize, необходимо воспользоваться методами IsLoading или IsStoring класса CArchive.
Итак, при создании нового класса, в котором метод Serialize применяется для сериализации данных, необходимо:
Чтобы класс был производным от класса CObject или его потомков.
При объявлении класса необходимо вставить макрокоманду DECLARE_SERIAL.
Определить в классе функцию Serialize, отвечающую за хранение переменных класса.
Определить в классе конструктор без параметров. Это может быть защищенный конструктор, если он вызывается только процесса сериализации данных. В конструкторе возможно динамическое создание объектов и инициализация переменных, если это необходимо.
Объявить в классе деструктор, если требуется выполнить специальные действия при разрушении объектов класса, например, освободить память динамически созданных объектов.
При реализации класса в начало файла реализации вставить макрос IMPLEMENT_SERIAL. У этой макрокоманды три параметра: имя класса, имя базового класса и номер версии формата файла, хранящего документ. Можно иметь несколько модификаций документа и с каждой из них связать свою версию.
Приведем шаблон (заготовку) файлов определения и реализации класса, который обеспечивает процесс сериализации данных:
// фрагмент файла определения класса class CMyDoc:public CObject { DECLARE_SERIAL(CMyDoc) protected: virtual void Serialize(CArchive& ar); protected: CMyDoc(); protected: ~CMyDoc(); // другие описания класса ........................ };
// фрагмент файла реализации класса IMPLEMENT_SERIAL(CMyDoc, CObject,1) CMyDoc::CMyDoc() { // здесь возможно динамическое создание объектов и // инициализация переменных, если это необходимо ………… } CMyDoc::~CMyDoc() { // здесь возможно выполнение специальных действий // при разрушении объектов класса, например, // освобождение памяти динамически созданных объектов ……… } void CMyDoc::Serialize(CArchive& ar) { if(ar.Storing()) { // здесь следует добавить код для записи переменных в архив ……… } else { // здесь следует добавить код для чтения переменных из архива, ……… } // здесь следует добавить вызовы методов Serialize для переменных // класса CMyDoc, являющихся объектами классов, // имеющих собственные методы Serialize } // другие методы класса .......................
Если клиенту нужен только один объект, то проще всего создать его с помощью CoCreateInstance. И все же случается, что клиенту может понадобиться много экземпляров объектов одного и того же класса. Чтобы их создание выполнялось эффективно, клиент может получить доступ к фабрике класса (class factory) — объекту, способному создавать другие объекты. Каждая фабрика класса знает, как создавать объекты одного конкретного класса (хотя название "class factory" не вполне удачно — ведь эти фабрики создают экземпляры классов, а не классы). Фабрики классов — полноценные объекты СОМ: доступ к ним осуществляется через интерфейсы, они поддерживают IUnknown и т.д. И все же они необычные объекты, так как могут создавать другие объекты СОМ.
Дело в том, что все объекты, с которыми мы встречались до сих пор, созданы фабрикой класса. Даже когда клиент просто вызывает CoCreateInstance, реализация этой функции в библиотеке СОМ создает объект с помощью фабрики класса. CoCreateInstance скрывает эти детали от клиента. Но на самом деле она использует методы интерфейса IClassFactory, описываемые ниже.
Интерфейс IClassFactory
Чтобы называться фабрикой класса, объект должен поддерживать интерфейс IClassFactory. Этот замечательно простой интерфейс содержит только 2 метода:
CoCreateInstance создает новый экземпляр класса, объекты которого может создавать данная фабрика. Клиент не передает этому методу в качестве параметра CLSID, так как класс объекта неявно определяется самой фабрикой. И все же клиент задает IID, чтобы получить указатель на нужный ему интерфейс. (Реализация этого метода фабрикой класса, написанной на C++, может создавать новый объект с помощью оператора C++ new.)
LockServer позволяет клиенту сохранить сервер загруженным в память. Объект-фабрика, как и другие объекты, поддерживает собственный счетчик ссылок, для учета количества использующих его клиентов. Однако по разным (очень сложным) соображениям этого счетчика недостаточно, чтобы удерживать сервер загруженным в память.
Чтобы сервер гарантированно продолжал работать, можно использовать IClassFactory::LockServer.
В некоторых случаях интерфейс IClassFactory слишком прост. На сегодня имеется новый интерфейс IClassFactory2, добавляющий новые возможности. Так как IClassFactory2 наследует от IClassFactory, в его состав входят методы Createlnstance и LockServer, Однако он поддерживает и еще несколько методов, связанных с лицензированием. Используя эти методы, можно разрешить создание новых объектов только лицензированным клиентам — таким, на чьих компьютерах установлена легальная, предположительно оплаченная копия программного обеспечение. Так как данная возможность особенно полезна для управляющих элементов ActiveX.
Использование фабрики класса
Чтобы получить доступ к фабрике класса, клиент вызывает функцию библиотеки СОМ CoGetClassObject. Этой функции передается CLSID класса объектов, которые будут создавать фабрики, а не CLSID самой фабрики. Клиент задает также IID интерфейса, нужного ему для работы с фабрикой. Конечно, обычно это IID интерфейса IClassFactory. Кроме того, как и в случае с CoCreateInstance, клиент может также задать тип сервера, который должен быть запущен для фабрики и ее объектов. Если для фабрики запрашивается, например, сервер "в процессе", то и объекты, созданные фабрикой, тоже будут выполняться данным сервером "в процессе".
Продемонстрируем использование фабрики класса. Допустим, клиент уже вызвал CoGetClassObject, библиотека СОМ запустила фабрику класса и возвратила указатель на интерфейс IClassFactory этой фабрики. Получив указатель, клиент вызывает метод IClassFactory::Createlnstance данного интерфейса (шаг 1). Среди параметров этого вызова клиент передает IID интерфейса, указатель на который ему необходим. В ответ фабрика класса создает объект (шаг 2) и возвращает клиенту указатель на заданный интерфейс (шаг 3). Теперь клиент может использовать возвращенный ему указатель для вызовов методов интерфейса (шаг 4).
Когда пользователь выбирает из меню File строки New, вызывается виртуальный метод OnNewDocument, определенный в базовом классе CDocument. Если этот метод не переопределяется, то по умолчанию он вызывает метод DeleteContents и далее помечает документ как чистый (пустой).
Можно переопределить метод OnNewDocument в наследуемом классе документа, чтобы выполнить его инициализацию (инициализацию его переменных, выделение при необходимости памяти). При этом требуется из переопределенного метода OnNewDocument сначала вызвать метод OnNewDocument, определенный в базовом классе.
BOOL CDoc::OnNewDocument() { // Вызов метода базового класса if (!CDocument::OnNewDocument()) return FALSE;
// Подготовка документа – инициализацию его переменных, // выделение при необходимости памяти для хранения данных ………
return TRUE; }
Когда пользователь создает новый документ в приложении, построенном на основе однооконного интерфейса, то на самом деле используется старый документ. Новый объект класса, представляющего документ, не создается. Метод OnNewDocument должен удалить содержимое документа и выполнить повторную инициализацию существующего объекта класса документа.
Из этого следует, что нельзя выполнять инициализацию документа в конструкторе класса документа, так как конструктор будет вызван только один раз за время работы приложения. Более правильно использовать для цели метод OnNewDocument.
Если в наследуемом классе документа переопределить метод DeleteContents, то метод OnNewDocument базового класса CDocument, будет вызывать метод DeleteContents, определенный в наследуемом классе. Переопределенный метод DeleteContents обычно имеет следущий вид:
void CDoc::DeleteContents() { // Очистка документа – освобождение памяти, // выделенной для хранения данных ……
// Вызов метода базового класса CDocument::DeleteContents(); }
Теперь в приложении при выборе из меню File строки New содержимое окна просмотра и документа обновляется. Приложение готово, например, выводить на экран новое изображение, создаваемое пользователем.
Некоторые многооконные приложения позволяют одновременно работать с документами различных типов.
Для создания нового класса документа, например COtherDoc, на основе базового класса CDocument используется ClassWizard. Это средство создает файл otherdoc.h с описанием класса COtherDoc и файл otherdoc.cpp, содержащий реализацию нового класса документа.
Для создания класса окна просмотра (COtherView) также используется средство ClassWizard, при помощи которого можно создать класс COtherView на базе CView или производных от него, определенных в библиотеке MFC.
Можно создать меню для окна и без использования методов LoadFrame или Create класса CFrameWnd. Для этого необходимо создать объект класса CMenu и вызвать для него несколько методов.
Объект класса CMenu не является меню, он только представляет существующее меню типа HMENU. Можно создать объект класса как локальный, а после использования удалить (на меню как таковое это не повлияет).
После объявления объекта класса CMenu можно загрузить меню из ресурсов приложения при помощи метода LoadMenu. Этот метод загружает меню, заданное именем или идентификатором ресурса, и связывает его с соответствующим объектом класса CMenu.
После того, как меню “загружено”, его можно подключить к окну, воспользовавшись методом SetMenu, входящим в класс CWnd. Если необходимо просто удалить текущее меню, используемое окном, то методу SetMenu следует передать значение NULL.
После установки меню методом SetMenu и до того, как соответствующий объект CMenu будет удален, необходимо вызвать метод Detach класса CMenu. Этот разорвет связь между меню и соответствующим объектом класса CMenu, после чего последний может быть удален.
Следует отметить, что если до установки нового меню окно уже имело меню, надо удалить его, воспользовавшись методом DestroyMenu класса CМenu.
Если с меню, подлежащим удалению, не связан объект класса CMenu, можно обратиться к методу Attach класса CMenu, который устанавливает связь между меню типа и объектом класса CMenu.
Для определения идентификатора меню типа известного окна можно воспользоваться методом GetMenu класса CWnd. Этот метод возвращает указатель на объект типа CMenu. Теперь для получения идентификатора меню можно обратиться к элементу данных m_hMenu, входящему в класс CMenu.
Рассмотрим фрагмент программы (в реализации класса CMyFrame, наследуемого от CFrameWnd), в котором происходит замена старого меню на новое:
void CMyFrame::OnChangeMenu() { // удаление старого меню // получаем указатель на текущее меню окна CMenu *pMenu=this->GetMenu(); // связываем меню HMENU с объектом menuCurrent CMenu menuCurrent.Attach(pMenu->m_hMenu); // удаляем меню и все связанные с ним ресурсы процесса menuCurrent.DestroyMenu();
// установление нового меню // загружаем меню IDR_MENU CMenu menuNew=LoadMenu(IDR_MENU); // устанавливаем загруженное меню this->SetMenu(&menuNew); // разрываем связь меню с объектом menuNew menuNew.Detach(); }
Класс CMenu помимо методов создания меню содержит и методы управления меню. Используя эти методы, можно добавлять к меню новые строки, изменять и удалять их. Специальные методы класса CMenu позволяют выделять некоторые строки меню и создавать элементы меню, содержащие не только текст, но и изображение.
При выборе пользователем пункта меню окну приходит соответствующее сообщение. Для его обработки следует предусмотреть специальные методы класса окна, при этом в таблицу сообщения класса окна необходимо добавить соответствующие макрокоманды типа ON_COMMAND.
До этого места в изложении материала предполагалось, что клиент уже как-то получил первый указатель на один из интерфейсов исполняющегося объекта. Конкретный способ получения клиентом этого указателя не обсуждался. В действительности способов несколько. Например, указатель может быть передан другим клиентом, либо клиент может получить его от моникера. Тем не менее для каждого объекта имеется некоторой клиент, создающий его и получающий самый первый указатель интерфейса. В конечном счете данный процесс основывается на использовании функций библиотеки СОМ.
Рассмотрим самый простой способ создания одного неинициализированного экземпляра объекта. Вначале клиент вызывает функцию библиотеки СОМ CoCreateInstance. Кроме других параметров, данный вызов задает CLSID объекта, который должен быть создан, а также IID некоторого интерфейса, поддерживаемого объектом. Далее библиотека СОМ по CLSID находит в системном реестре запись, соответствующую классу данного объекта. (Точнее, библиотека СОМ предоставляет выполнение этой задаче Диспетчеру управления сервисами, или SCM – Service Control Manager.) Эта запись содержит информацию о местоположении сервера, способного создать экземпляр класса объекта. После того как сервер найден, SCM запускает его.
Вместе с CLSID и IID первого интерфейса, указатель которого необходим клиенту, параметры CoCreateInstance позволяют также клиенту указать, какой тип сервера должен быть запущен библиотекой СОМ — например, "в процессе" или локальный. Клиент имеет право сказать, что тип сервера ему безразличен, либо задать любую комбинацию допустимых типов серверов.
Запущенный сервер создает экземпляр класса объекта и возвращает указатель на запрошенный интерфейс библиотеке СОМ. Последняя в свою очередь передает данный указатель клиенту, который затем может выполнять вызовы методов этого интерфейса. Так как результатом данного процесса является создание неинициализированного объекта, то клиент обычно запрашивает интерфейс, через который объект может быть инициализирован, хотя это и не обязательно.
До этого места подразумевалось, что создаваемый объект реализован в сервере "в процессе" или локальном, т.е. будет выполняться на той же машине, что и клиент. Что же происходит в случае удаленного сервера? Как создается удаленный экземпляр объекта?
Поддержка удаленных объектов обеспечивается DCOM. Процесс во многом аналогичен созданию локального объекта: клиент выполняет тот же вызов библиотеки СОМ, SCM просматривает системный реестр и т.д. Если же указан удаленный сервер, СОМ установит для создания экземпляра объекта связь с удаленной машиной.
Как и все межмашинные коммуникации в DCOM, данный запрос выполняется вызовом удаленной процедуры. Просмотрев свой реестр, удаленная система находит исполняемый файл сервера и создает экземпляр объекта. Так же, как и в случае локального сервера, возвращается указатель на интерфейс, после чего клиент может вызывать методы вновь созданного объекта. Для клиента запуск объекта выполняется одинаково независимо от того, каким сервером реализован объект: "в процессе", локальным или удаленным; данное различие должно учитываться клиентом, лишь когда он сам считает это необходимым.
Независимо от типа запускаемого сервера СОМ устанавливает правила защиты, определяющие, какими клиентами может быть запущен тот или иной сервер. Кроме того, СОМ задает интерфейсы и функции библиотеки СОМ для поддержки контроля прав доступа, хотя точная их реализация зависит от операционной системы. Доступ к распределенным сервисам защиты для удаленных серверов определяется DCOM.
Процесс создания панели состояния во многом схож с процессом создания панелей управления.
Сначала надо создать объект класса CStatusBar - он будет представлять панель состояния и управлять ею. Обычно для этого включают объект класса CStatusBar непосредственно в класс окна приложения, в котором будет размещена эта панель состояния:
class CMainFrame : public CMDIFrameWnd { protected: CStatusBar m_wndStatusBar; // панель состояния // другие описания класса ....... };
В некоторых случаях вместо использования класса CStatusBar от него предварительно наследуется дополнительный класс. В этом случае для создания панели состояния используют именно этот класс.
Следующим шагом является создание самой панели состояния и связывание ее с объектом, ее представляющим. Панель состояния создается вызовом метода Create класса CStatusBar:
BOOL Create( CWnd* pParentWnd, DWORD dwStyle = WS_CHILD | WS_VISIBLE | CBRS_BOTTOM, UINT nID = AFX_IDW_STATUS_BAR );
Через первый параметр указывается окно (родительское окно), для которого создается панель состояния.
Второй параметр позволяет задать характеристики панели управления, в том числе ее расположение внутри окна. Панель состояния является дочерним окном, поэтому в параметре стиля следует указать атрибут WS_CHILD. Атрибут WS_VISIBLE также следует указать, так как при его отсутствии панель не появится на экране, хотя и будет создана.
Панель состояния можно разместить либо вверху, либо внизу окна. Для этого в стиль панели состояния необходимо обязательно включить один из атрибутов CBRS_TOP или CBRS_BOTTOM.
Последний параметр метода Create определяет идентификатор дочернего окна панели состояния. По умолчанию используется идентификатор AFX_IDW_STATUS_BAR.
Следует заметить, что приложения, созданные MFC AppWizard, имеют меню View, содержащие строки “Toolbar” и “Status bar”. Эти строки позволяют показывать и скрывать панели управления и состояния в главном окне приложения. Для обработки сообщений о выборе строки меню “Status bar” используется метод OnUpdateControlBarMenu класса CFrameWnd.
Причем этот метод может управлять отображением панели состояния, если только она имеет идентификатор AFX_IDW_STATUS_BAR.
После того, как панель состояния создана, следует определить ее внешний вид. Для достижения этой цели сначала создается массив идентификаторов, представляющих индикаторы панели состояния и определяющих информацию, которая должна отображаться в панели состояния. Идентификатор представляет текстовую строку из таблицы ресурсов приложения.
Каждый элемент панели состояния (индикатор) должен иметь отдельный идентификатор, например:
static UINT BASED_CODE indicators[] = { ID_SEPARATOR, // индикатор информации о состоянии ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL, };
Для того, чтобы связать каждый индикатор панели состояния с идентификатором текстовой строки (со строковым ресурсом), необходимо вызвать метод SetIndicators класса CStatusBar, передавая ему подготовленный массив идентификаторов . Этот метод загружает строковые ресурсы, соответствующие идентификаторам индикаторов, и размещает их на панели состояния. Через первый параметр передается указатель на массив идентификаторов индикаторов панели состояния. Общее количество индикаторов панели состояния, определенных в массиве идентификаторов, передается во втором параметре.
Обычно создание панели состояния производят при обработке сообщения WM_CREATE для главного окна приложения, например:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { // вызов метода базового класса для корректного создания окна if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1) return -1;
// создание панели управления (процесс создания рассматривался выше) ……… // создание панели состояния if (!m_wndStatusBar.Create(this) !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) return -1;
.......... return 0; }
Метод SetIndicators отображает в панели состояния строки, идентификаторы которых представлены в массиве indicators. При этом первый элемент массива определяет крайнюю левую строку в панели состояния.По умолчанию, строки в панели состояния выравниваются по правой границе. Можно получить доступ к любому элементу панели состояния по его индексу или идентификатору.
Для того, чтобы определить, какой идентификатор имеется у того или другого индикатора, можно воспользоваться методом GetItemID. Этот метод возвращает идентификатор индикатора по его индексу (порядковому номеру в панели состояния). Обратная операция выполняется при помощи метода CommandToIndex.
Для работы с панелями управления в состав библиотеки MFC включены два класса - CToolBar и CDialogBar. Они оба наследуются от базового класса CControlBar, реализующего основные функции панели управления. Кроме того, от базового класса наследуется еще один класс - CStatusBar. Он предназначен для работы с панелью состояния и будет рассматриваться позже.
Класс CToolBar представляет панель управления, состоящую из кнопок. При желании можно в панель управления класса CToolBar помимо кнопок включить и другие органы управления, например, списки или поля редактирования, однако такая возможность требует дополнительного программирования. Если необходимо создать панель, содержащую различные органы управления, а не только кнопки, то удобнее воспользоваться классом CDialogBar. Этот класс позволяет создать панель управления на основе шаблона диалоговой панели и будет рассматриваться позже.
Кнопки панели управления могут работать как кнопки, как переключатели и как переключатели с зависимой фиксацией (радио-кнопки). Тип кнопок панели управления выбирается методами класса CToolBar, например, метод SetButtonStyle.
Чтобы создать панель управления, необходимо сначала определить объект класса CToolBar, который будет представлять данную панель. Можно создать объект и нового класса, наследованного от CToolBar, в котором новыми дополнительными методами расширяются возможности класса CToolBar.
Обычно объект CToolBar включают как элемент главного окна приложения, например как элемент класса, наследованного от класса CFrameWnd (или CMDIFrameWnd - в зависимости от интерфейса приложения), например:
class CMainFrame : public CMDIFrameWnd { protected: CToolBar m_wndToolBar; // панель управления // другие описания класса ....... };
После того, как объект класса CToolBar образован, следует вызвать для него метод Create, который создает панель управления:
BOOL Create( CWnd* pParentWnd, DWORD dwStyle = WS_CHILD | WS_VISIBLE | CBRS_TOP, UINT nID = AFX_IDW_TOOLBAR );
В качестве параметров методу Create указываются различные характеристики создаваемой панели.
Только первый параметр является обязательным - в нем указывается идентификатор родительского окна для панели управления.
Необязательный параметр dwStyle определяет, как будет отображаться панель управления. Рассмотрим некоторые флаги, комбинации которых можно использовать для задания характеристик панели:
CBRS_BOTTOM - панель отображается в нижней части окна.
CBRS_FLOATING - панель отображается в отдельном окне.
CBRS_FLYBY - панель состояния отображает краткое описание выбранной кнопки.
CBRS_SIZE_DYNAMIC - размер панели можно изменять. При этом кнопки в панели могут перестраиваться в несколько рядов.
CBRS_SIZE_FIXED - размер панели нельзя изменять.
CBRS_TOOLTIPS - краткое описание кнопки отображается в окне tool tips.
CBRS_TOP - панель отображается в верхней части окна.
Через последний параметр метода Create передается идентификатор, который будет присвоен панели управления. По умолчанию используется идентификатор AFX_IDW_TOOLBAR.
Следует заметить, что приложения, созданные MFC AppWizard, имеют меню View, содержащие строки “Toolbar” и “Status bar”. Эти строки позволяют показывать и скрывать панели управления и состояния в главном окне приложения. Для обработки сообщений о выборе строки меню “Toolbar” используется метод OnUpdateControlBarMenu класса CFrameWnd. Причем этот метод может управлять отображением панели управления, если только она имеет идентификатор AFX_IDW_TOOLBAR.
После создания панели управления методом Create, необходимо загрузить ресурс панели управления. Для этого предназначен метод LoadToolBar. В качестве параметра этому методу следует указать или имя ресурса панели управления, либо идентификатор ресурса панели управления.
Во время создания панели управления можно указать ее характеристики. Эти характеристики можно изменить прямо во время работы приложения с помощью метода SetBarStyle класса CControlBar. Параметр этого метода задает новые характеристики панели управления. В качестве этого параметра можно использовать комбинации флагов, некоторые из которых описаны ниже:
CBRS_ALIGN_TOP - панель можно отобразить в верхней части окна.
CBRS_ALIGN_BOTTOM - панель можно отобразить в нижней части окна.
CBRS_ALIGN_LEFT - панель можно отобразить в левой части окна.
CBRS_ALIGN_RIGHT - панель можно отобразить в правой части окна.
CBRS_ALIGN_ANY - панель можно отобразить в любой части окна.
CBRS_TOOLTIPS - краткое описание кнопки отображается в окне tool tips.
CBRS_FLYBY - панель состояния отображает краткое описание выбранной кнопки.
Для определения текущих характеристик панели управления используется метод GetBarStyle класса CControlBar.
Остов проекта создается при помощи средства MFC ActiveX ControlWizard (OLE ControlWizard) за два шага, на каждом из которых можно выбрать нужные опции. Следует отметить, что чаще всего принимаются значения, предлагаемые по умолчанию.
Средство ControlWizard создаст совокупность файлов, имена которых по умолчанию строятся с использованием имени проекта. К примеру, для проекта Name (все опции которого выбираются по умолчанию), создаются следующие основные файлы (см. ):
NameCtl.h, NameCtl.сpp - объявляют и реализуют класс CNameCtrl, производный от базового класса COleControl из библиотеки MFC. Он определяет базовые свойства и поведение OCX-объектов.
Name.h, Name.cpp - необходимы для инициализации DLL-файла, содержащего OCX-объект Name Control, его выгрузки и выполнения других инициализационных функций. Они определяют класс CNameApp, производный от COleControlModule. Методы InitInstance() b ExitInstance() этого класса создают экземпляр класса, регистрируют и инициализируют элемент управления.
NamePpg.h, NamePpg.cpp - определяют класс CNameProgPage, задающий базовую страницу свойств элемента управления.
NameCtl.bmp - содержит графический образ инструментальной кнопки, появляющейся на панели инструментов при добавлении OLE-элемента в новый проект, являющегося контейнером для OCX-объектов. По умолчанию есть некоторый стандартный образ элемента управления.
Name.rc, Resource.h - файл ресурсов проекта и его заголовочный файл.
Name.odl - содержит исходный код на языке описания объекта (Object Description Language), по которому Visual C++ генерирует доступную для других клиентов OLE Automation библиотеку типа, содержащую интерфейс элемента управления. Odl-файл элемента управления представляет собой исходный текст библиотеки типов элемента управления. Этот файл преобразовывается утилитой Make Type Library (mktypelib.exe) в библиотеку типов (файл с расширением tlb), которая будет в виде ресурса включена в ocx-файл.
Рассмотрим классы, создаваемые средством MFC ActiveX ControlWizard подробнее.
Для создания рабочего потока предназначена функция AfxBeginThreadбиблиотеки MFC:
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
Каждый поток внутри родительского процесса начинает свое выполнение с вызова специальной функции, называемой потоковой функцией. Выполнение потока продолжается до тех пор, пока не завершится его потоковая функция. Адрес данной функции (т.е. входная точка в поток) передается в параметре pfnThreadProc. Все потоковые функции должны иметь следующий прототип:
UINT pfnThreadProc(LPVOID pParam);
Значение параметра pParam функции AfxBeginThread передается потоковой функции в качестве параметра. Это 32-разрядное число может использоваться для любых целей.
Начальный приоритет потока указывается в параметре nPriority. Если этот параметр равен 0, то используются установки приоритета текущего (родительского) потока.
Каждый поток имеет свой собственный стек. Размер стека указывается в параметре nStackSize. Если этот параметр равен нулю (общепринятый подход), то создаваемому потоку будет выделен стек такого же размера, что и у родительского потока, а при необходимости, размер стека может быть увеличен.
Параметр dwCreateFlags определяет состояние выполнения потока. Если данный параметр равен нулю, поток начинает выполняться немедленно. Если значение этого параметра равно CREATE_SUSPEND, то поток создается временно приостановленным, т.е. ожидающим запуска. Чтобы запустить такой поток, нужно вызвать функцию CWinThread::ResumeThread.
Параметр lpSecurityAttrs является указателем на набор атрибутов прав доступа, относящийся к данному потоку. Если этот параметр равен NULL, то набор атрибутов будет унаследован от родительского окна.
При успешном завершении функция AfxBeginThread возвращает указатель на объект потока, в противном случае возвращает ноль. Данный указатель необходимо сохранять, если впоследствии предполагается обращение из родительского потока к созданному потоку (например, для изменения приоритета или для временного приостановления потока).
Рассмотрим создание сервера процесса, предоставляющего класс под названием SimpleATL.
Среди проектов Developer Studio следует выбрать пункт ATL СОМ AppWizard и в поле “Project Name” ввести имя проекта SvrDll (или SvrExe в случае локального сервера). Мастер ATL СОМ AppWizard создаст сервер за один шаг. Следует только в появившейся диалоговой панели отметить обе предлагаемые дополнительные опции “Allow merging of proxy/stub code” и “Support MFC”, а затем выбрать тип сервера “Dynamic Link Library (DLL)” (сервер в процессе) или “Executable (EXE)” (локальный сервер). Заметим, что включение опции “Allow merging of proxy/stub code” позволяет мастеру разместить в COM-сервере вспомогательный код для MIDL.
Мастер ATL СОМ AppWizard елает еще меньше, чем мастер из библиотеки MFC: он вообще не генерирует никаких классов, только глобальные объявления, необходимые для создания файла DLL. Все остальное разработчику необходимо выполнить самостоятельно.
Шаблоны документов, с которыми работает приложение, определяют все характеристики данного типа документа. Они включают информацию о классе документа, классе окна просмотра, классе окна рамки, а также о ресурсах, связанных с данным типом документа.
Шаблоны документов создаются объектом приложения во время его инициализации. Если, например, посмотреть на метод InitInstance главного класса приложения multi, то видно, что в нем создается только один объект класса CMultiDocTemplate, который представляет графический документ и средства работы с ним. Если необходимо, чтобы многооконное приложение работало и с другим типами документов, необходимо в методе InitInstance создать объект шаблона документов этих типов и добавить его в список шаблонов. Например:
BOOL CMultiApp::InitInstance() { ....... CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_MULTITYPE,RUNTIME_CLASS(CMultiDoc), RUNTIME_CLASS(CChildFrame),RUNTIME_CLASS(CMultiView)); AddDocTemplate(pDocTemplate);
pDocTemplate = new CMultiDocTemplate( IDR_OTHERTYPE,RUNTIME_CLASS(COtherDoc), RUNTIME_CLASS(CChildFrame),RUNTIME_CLASS(COtherView)); AddDocTemplate(pDocTemplate);
.... return TRUE; }
При создании шаблона документов указывается идентификатор, который определяет меню, пиктограмму и некоторую другую полезную информацию, связанную с документами данного типа. В методе InitInstance класса в качестве шаблона текстового документа указан идентификатор IDR_OTHERTYPE (такой идентификатор еще не определен, о ресурсах текстового документа речь пойдет далее).
Чтобы созданный шаблон текстовых документов добавить к списку шаблонов документов приложения, надо вызвать метод AddDocTemplate, указав ему адрес объекта шаблона.
Сначала при помощи средства AppWizard создается шаблон однооконного или многооконного приложения. При этом генерируются следующие основные классы:
главный класс приложения (производный от базового класса CWinApp);
класс главного окна приложения (производный от базового класса CFrameWnd или CMDIFrameWnd в зависимости от типа приложения);
класс документа приложения (производный от базового CDocument);
класс окна просмотра документа (производный от базового CView);
класс дочернего MDI-окна (производный от базового класса CMDIChildWnd) – только для mdi-приложения.
Сначала необходимо выбрать тип создаваемого приложения из предоставляемого списка: “Single document” (приложение с однооконным интерфейсом), “Multiple document” (приложение с многооконным интерфейсом) или “Dialog based” (приложение, основанное на диалоговой панели без главного окна). После выбора типа приложения “Dialog based” появится следующая диалоговая панель, предназначенная для определения основных свойств приложения.
В этой панели можно будет указать, будет ли у создаваемого приложения информационная диалоговая панель, справочная подсистема, трехмерные органы управления, возможности использования технологии OLE и коммуникации с помощью протокола TCP/IP. Здесь же можно задать заголовок главной диалоговой панели.
В следующей диалоговой панели можно либо указать на необходимость включения комментариев к создаваемому шаблону приложения, либо отменить этот режим. Здесь же можно отметить желаемый способ подключения библиотеки MFC - в виде статически или динамически подключаемой библиотеки.
На последней диалоговой панели можно задать имена для создаваемых системой AppWizard классов приложения.
После определения всех свойств приложения MFC AppWizard создаст проект, который можно сразу же оттранслировать и получить приложение, полностью готовое к запуску. После запуска приложения на экране появляется диалоговая панель с двумя кнопками “OK” и “Cancel” и статическим полем.
Проект dlg включает в себя ряд файлов, созданных системой AppWizard (см. ). Рассмотрим назначения этих файлов:
dlg.h - в этом файле перечислены другие включаемые файлы и описан главный класс приложения CDlgApp.
dlg.cpp - основной файл приложения. В нем определены методы основного класса приложения CDlgApp.
dlgDlg.h - содержит описание класса главной диалоговой панели, который называется CDlgDlg. Класс CDlgDlg наследуется от базового класса CDialog, определенного в библиотеке классов MFC.
dlgDlg.cpp - файл содержит определение методов класса CDlgDlg.
dlg.rc - файл ресурсов. В этом файле описаны все ресурсы приложения.
Сами ресурсы приложения могут быть записаны в каталоге Res, расположенном в главном каталоге проекта.
resource.h - файл содержит определения идентификаторов ресурсов приложения.
res\dlg.ico - пиктограмма приложения.
res\dlg.rc2 - в этом файле определены ресурсы, которые нельзя редактировать с помощью редактора ресурсов среды Visual C++.
StdAfx.h, StdAfx.cpp - использование этих файлов позволяет ускорить процесс повторного построения проекта.
readme.txt - текстовый файл, содержащий описание проекта. В нем кратко рассмотрен каждый файл, входящий в проект, перечислены классы приложения, а также представлена другая информация.
Созданный при помощи AppWizard проект можно взять за основу, изменить его в соответствии с потребностями и задачами приложения. В диалоговую панель можно добавить новые органы управления, создать другие ресурсы. Изменять подготовленный проект можно с использованием средства ClassWizard (для добавления новых классов, новых методов и переменных для существующих классов, для создания методов-обработчиков сообщений и т.д.) и редактора ресурсов.
Сервисы создания объектов — одни из важнейших сервисов, предоставляемых СОМ. Клиенты обычно создают объекты, вызывая библиотеку СОМ или через моникеры. Эти подходы работают и в DCOM, хотя и с некоторыми новыми особенностями. Рассмотрим различные варианты создания объектов, доступные клиентам.
Перед тем как непосредственно перейти к созданию серверов ActiveX, важно разобраться в причинах, по которым Microsoft выпустила набор шаблонов для создания компонентов ActiveX (включая серверы автоматизации, полные и мини-серверы, контейнеры и элементы управления).
Библиотека MFC среды Developer Studio представляет собой прекрасную иерархию классов, но в некоторых случаях пользоваться ею неудобно. В частности, это касается создания небольших компонентов ActiveX, предназначенных для использования в Web. Глупо тратить время на загрузку нескольких DLL, которые оказываются ненужными. Если же статически связать элемент управления MFC или компонент, то получится очень большой файл EXE, DLL или ОСХ.
Итак, после продолжительных дебатов было решено создать новую библиотеку, названную библиотекой активных шаблонов (Active Template Library, ATL). Впервые она была включена в Microsoft Visual C++ 4.2.
Библиотека ATL является прекрасным решением для создания небольших компонентов, поскольку оно не связано с использованием библиотеки MFC (если только программист специально не укажет включить такую поддержку).
Недостатком библиотеки ATL является нехватка специалистов, с которыми можно проконсультироваться, и дефицит практических примеров, помогающих в ее освоении. Кроме того, отсутствует возможность использовать мастер Class Wizard для создания приложения. К счастью, можно еще использовать мастера СОМ Wizard и несколько диалоговых окон для добавления классов, методов и свойств.
Какой способ использовать — решать программисту, но настоятельно рекомендуется при возможности использовать библиотеку ATL, поскольку она обладает рядом преимуществ, например, легкостью в создании серверов автоматизации.
Мини-сервера или полные сервера, а также контейнеры составных документов, лучше всего разрабатывать с помощью MFC, которая обеспечивает работу с документами на высоком уровне. Напомним, что выбор типа приложения (контейнер или сервер, тип сервера) осуществляется в третьей диалоговой панели, которая появляется в процессе работы с мастером MFC AppWizard (EXE) при создании SDI- или MDI-приложения.
В состав библиотеки MFC входит ряд классов, представляющих стандартные диалоговые панели. Эти классы позволяют легко реализовать такие часто используемые операции, как открытие и сохранение файла, выбор цвета, выбор шрифта и т.д. Все эти классы наследуются от класса CCommonDialog, который в свою очередь является производным по отношению к базовому классу CDialog.
Приведем классы стандартных диалоговых панелей и их назначение:
CColorDialog - Панель для выбора цвета
CFileDialog - Панель выбора файлов для открытия и сохранения на диске
CFindReplaceDialog - Панель для выполнения операции поиска и замены
CFontDialog - Панель для выбора шрифта
CPrintDialog - Панель для вывода документа на печать
CPageSetupDialog - Панель выбора формата документа
COleDialog - Панель для управления технологией OLE
Классы, управляющие стандартными диалоговыми панелями, определены в файле afxdlgs.h. Поэтому при использовании этих классов в приложении необходимо включить этот файл в исходный текст при помощи директивы #include.
Большинство приложений, созданных на основе MFC, использует ряд стандартных командных сообщений, как правило, соответствующих элементам меню или кнопкам панели управления. К ним относятся командные сообщения для завершения работы приложения, создание нового документа, открытия документа, записанного на диске, сохранение документа на диске, вызова справочной системы, управления текстовым редактором и т.д. За каждым таким сообщением зарезервирован отдельный идентификатор.
MFC обеспечивает различный уровень обработки стандартных командных сообщений, начиная от простого резервирования идентификатора до полной обработки отдельных сообщений.
Элемент меню или кнопка панели управления приложения имеет тот же идентификатор, что и командное сообщение. Ниже коротко описаны наиболее важные командные сообщения.
Командные сообщения с идентификаторами ID_FILE_. Данные командные сообщения соответствуют элементам меню File приложений, созданных при помощи средств MFC AppWizard. Обработчики этих сообщений входят в состав различных классов MFC, в том числе CWinApp и CDocument.
Командные сообщения с идентификаторами ID_EDIT_. Эти сообщения соответствуют элементам меню Edit приложений, созданных при помощи средств MFC AppWizard. Это меню обычно используется для выполнения различных операций над документом, отображаемым в окне просмотра.
Класс CEditView содержит обработчики для командных сообщений ID_EDIT_. Если в приложении наследуется класс окна просмотра от базового класса CEditView, то меню Edit будет работать.
Класс CView не содержит стандартных обработчиков для командных сообщений, имеющих идентификаторы ID_EDIT_. Программист должен самостоятельно реализовать их в своем окне просмотра.
Командные сообщения с идентификаторами ID_WINDOW_. Данные сообщения соответствуют элементам меню Window многооконных приложений, созданных при помощи средств MFC AppWizard. Обработка этих командных сообщений возложена на метод OnMDIWindowCmd класса CMDIFrameWnd.
Командные сообщения с идентификаторами ID_APP_. В MFC определены два командных сообщения с идентификаторами ID_APP_. Они предназначены для завершения приложения и вывода информации о приложении и его авторе.
Командные сообщения с идентификаторами ID_HELP_. Данные сообщения используются справочной системой приложения.
Класс CWinApp содержит методы для обработки командных сообщений, связанный со справочной системой. Если в приложении используется справочная система, программист должен сам вызывать соответствующие методы класса CWinApp для обработки командных сообщений ID_HELP_.
MFC AppWizard позволяет создавать приложение, имеющее справочную систему. В этом случае MFC AppWizard автоматически создает программный код, необходимый для управления справочной системой.
Командные сообщения с идентификаторами ID_VIEW_. Эти командные сообщения соответствуют элементам меню View приложений, созданных при помощи средств MFC AppWizard. За обработку командных сообщений ID_VIEW_ отвечает класс CFrameWnd.
Ниже описаны стандартные последовательности обработки командных сообщений объектами различных классов.
Главное окно многооконного приложения. Большинство командных сообщений передаются главному окну приложения. Для приложений, имеющих многооконный интерфейс, роль главного окна приложения выполняет объект класса CMDIFrameWnd или объект класса, наследованный от базового CMDIFrameWnd.
Получив сообщение, главное окно приложения сначала предоставляет возможность обработать сообщение активному дочернему окну MDI. Дочерние окна MDI представляют собой объекты класса CMDIChildWnd или класса, наследованного от него.
И только если окно MDI не может обработать сообщение, проверяется таблица сообщений класса главного окна приложения. Следует отметить, что, в свою очередь, окно MDI передает сообщения другим объектам.
Если главное окно приложения также не может обработать командное сообщение, оно передается объекту главного класса приложения. Главный класс приложения наследуется от базового класса CWinApp и приложение имеет только один объект этого класса.
Окна MDI и главное окно однооконного приложения. Для приложений, имеющих однооконный интерфейс, роль главного окна приложения выполняет объект класса CFrameWnd или класса, наследованного от него.
Главное окно однооконного приложения и дочерние MDI-окна многооконного приложения обрабатывают командные сообщения одинаклвым образом. Объект класса CFrameWnd или CMDIChildWnd, которому поступило командное сообщение, передает его соответствующему окну просмотра. Если оно просмотра не может обработать сообщение, проверяется таблица сообщений классов CFrameWnd или CMDIChildWnd.
Если главное окно однооконного приложения или MDI-окно многооконного приложения не может обработать сообщение, оно передается объекту главного класса приложения.
Окна просмотра. В отличие от объектов, представляющих окна типа frame (объекты классов CFrameWnd, CMDIFrameWnd и CMDIChildWnd), окно просмотра в первую очередь проверяет собственную таблицу сообщений.
И только в том случае, если командное сообщение не может быть обработано, оно передается документу, связанному с данным окном просмотра.
Документ. Так же как и окно просмотра, объект, представляющий документ, сначала проверяет свою таблицу сообщений. Только в том случае, если в классе документа отсутствует обработчик командного сообщения, оно передается для обработки шаблону данного документа.
Объект, представляющий шаблон документа, проверяет только собственную таблицу сообщений и не передает командные сообщения другим объектам приложения.
Диалоговая панель. Диалоговые панели представляются объектами классов, наследованных от базового класса CDialog, Если командное сообщение, поступившее объекту диалоговой панели, не может быть обработано, оно передается его родительскому окну.
Если родительское окно диалоговой панели также не может обработать командное сообщение, оно передается главному объекту приложения.
// stdafx.cpp : source file that includes just the standard includes // dlg.pch will be the pre-compiled header // stdafx.obj will contain the pre-compiled type information #include "stdafx.h"
// stdafx.cpp : source file that includes just the standard includes // single.pch will be the pre-compiled header // stdafx.obj will contain the pre-compiled type information #include "stdafx.h"
// stdafx.cpp : source file that includes just the standard includes // multi.pch will be the pre-compiled header // stdafx.obj will contain the pre-compiled type information
#include "stdafx.h"
// stdafx.cpp : source file that includes just the standard includes // stdafx.pch will be the pre-compiled header // stdafx.obj will contain the pre-compiled type information #include "stdafx.h"
// stdafx.h : include file for standard system include files, or project specific include // files that are used frequently, but are changed infrequently #if !defined(AFX_STDAFX_H__A720366A_E01B_11D1_9525_0080488929D2__INCLUDED_) #define AFX_STDAFX_H__A720366A_E01B_11D1_9525_0080488929D2__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers #include <afxwin.h> // MFC core and standard components #include <afxext.h> // MFC extensions #ifndef _AFX_NO_AFXCMN_SUPPORT #include <afxcmn.h> // MFC support for Windows Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line. #endif // !defined(AFX_STDAFX_H__A720366A_E01B_11D1_9525_0080488929D2__INCLUDED_)
// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #if !defined(AFX_STDAFX_H__A7203676_E01B_11D1_9525_0080488929D2__INCLUDED_) #define AFX_STDAFX_H__A7203676_E01B_11D1_9525_0080488929D2__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers #include <afxwin.h> // MFC core and standard components #include <afxext.h> // MFC extensions #ifndef _AFX_NO_AFXCMN_SUPPORT #include <afxcmn.h> // MFC support for Windows Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line. #endif // !defined(AFX_STDAFX_H__A7203676_E01B_11D1_9525_0080488929D2__INCLUDED_)
// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently //
#if !defined(AFX_STDAFX_H__A720368A_E01B_11D1_9525_0080488929D2__INCLUDED_) #define AFX_STDAFX_H__A720368A_E01B_11D1_9525_0080488929D2__INCLUDED_
#if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000
#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers
#include <afxwin.h> // MFC core and standard components #include <afxext.h> // MFC extensions #ifndef _AFX_NO_AFXCMN_SUPPORT #include <afxcmn.h> // MFC support for Windows Common Controls #endif // _AFX_NO_AFXCMN_SUPPORT
//{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_STDAFX_H__A720368A_E01B_11D1_9525_0080488929D2__INCLUDED_)
#if !defined(AFX_STDAFX_H__EC72D080_FABF_11D1_9525_0080488929D2__INCLUDED_) #define AFX_STDAFX_H__EC72D080_FABF_11D1_9525_0080488929D2__INCLUDED_ #if _MSC_VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 // stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but are changed infrequently #define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers #include <afxctl.h> // MFC support for ActiveX Controls // Delete the two includes below if you do not wish to use the MFC // database classes #include <afxdb.h> // MFC database classes #include <afxdao.h> // MFC DAO database classes //{{AFX_INSERT_LOCATION}} // Microsoft Developer Studio will insert additional declarations immediately before the previous line. #endif // !defined(AFX_STDAFX_H__EC72D080_FABF_11D1_9525_0080488929D2__INCLUDED_)
Как уже отмечалось, OLE-элемент управления может следить за изменением значений своих свойств. При втором способе реализации строится специальная функция уведомления, вызываемая при изменении значения. При третьем и четвертом способе для изменения значения всегда вызывается метод Set. Так что почти для всех способов реализации свойства элемент управления может фиксировать изменение значения своих свойств.
Иногда бывает важно, чтобы и контейнер мог следить за этими изменениями. Свойство называется связываемым (bindable), если имеет потенциальную возможность сообщить контейнеру об изменении своего значения.
Связывание делается в два этапа. На первом свойство добавляется, как это было описано, любым способом реализации, кроме первого. На втором этапе добавленное свойство получает статус связываемого. Для этого следует:
Вызвав ClassWizard, выбрать вкладку OLE Automation.
В окне списка “External Name” выбрать имя свойства.
Щелкнуть кнопку “Data Binding”.
В появившемся окне включить флажок “Bindable Proprty” и выбрать кнопку “OK”.
Теперь свойство получило потенциальную возможность уведомление контейнера о своих изменениях. Реализовать эту возможность и превратить связываемое свойство в связанное (bound) свойство можно в процессе работы элемента управления в контейнере. Для этого нужно вызвать один из двух методов BoundPropertyChanged или BoundPropertyRequestEdit. Вызываются эти функции либо в методе Set, либо в функции уведомления.
Связывание называется оптимистическим (optimistic data binding), если вызывается метод BoundPropertyChanged. При этом изменение свойства всегда выполняются, и контейнер только уведомляется о нем. Если вызывается метод BoundPropertyRequestEdit, связывание назывется пессимистическим (pessimistic data binding). Для этого типа связывания изменение значения свойства производится, если только результат вызова функции TRUE, что означает разрешение контейнера на изменение значения.
В библиотеке классов MFC для обработки сообщений используется специальный механизм, который имеет название Message Map - таблица сообщений.
Таблица сообщений состоит из набора специальных макрокоманд, ограниченных макрокомандами BEGIN_MESSAGE_MAP и END_MESSAGE_MAP. Между ними расположены макрокоманды, отвечающие за обработку отдельных сообщений:
BEGIN_MESSAGE_MAP(ИмяКласса,ИмяБазовогоКласса) // макросы END_MESSAGE_MAP()
Макрокоманда BEGIN_MESSAGE_MAP представляет собой заголовок таблицы сообщений. Она имеет два параметра. Первый параметр содержит имя класса таблицы сообщений. Второй - указывает его базовый класс.
Если в таблице сообщений класса отсутствует обработчик для сообщения, оно передается для обработки базовому классу, указанному вторым параметром макрокоманды BEGIN_MESSAGE_MAP. Если таблица сообщений базового класса также не содержит обработчик этого сообщения, оно передается следующему базовому классу и т.д. Если ни один из базовых классов не может обработать сообщение, выполняется обработка по умолчанию, зависящая от типа сообщения:
стандартные сообщения Windows обрабатываются функцией обработки по умолчанию;
командные сообщения передаются по цепочке следующему объекту, который может обработать командное сообщение.
Можно определить таблицу сообщений класса вручную, однако более удобно воспользоваться для этой цели средствами ClassWizard. ClassWizard не только позволяет в удобной форме выбрать сообщения, которые должен обрабатывать класс. Он включит в состав класса соответствующие методы-обработчики. Программисту останется только вставить в них необходимый код. К сожалению, использовать все возможности ClassWizard можно только в том случае, если приложение создано с применением средств автоматизированного программирования MFC AppWizard.
Рассмотрим макрокоманды, отвечающие за обработку различных типов сообщений.
Макрокоманда ON_WM_<name>. Обрабатывает стандартные сообщения операционной системы Windows. Вместо <name> указывается имя сообщения без префикса WM_.
Например:
ON_WM_CREATE()
Для обработки сообщений, определенных в таблице макрокомандой On_WM_<name>, вызываются одноименные методы. Имя метода обработчика соответствует названию сообщения, без учета префикса WM_. В классе CWnd определены обработчики для стандартных сообщений. Эти обработчики будут использоваться по умолчанию.
Макрокоманды ON_WM_<name> не имеют параметров. Однако методы, которые вызываются для обработки соответствующих сообщений, имеют параметры, которые, количество и назначение которых зависит от обрабатываемого сообщения.
Если определить обработчик стандартного сообщения Window в своем классе, то он будет вызываться вместо обработчика, определенного в классе CWnd (или другом базовом классе). В любом случае можно вызвать метод-обработчик базового класса из своего метода-обработчика.
Макрокоманда ON_REGISTERED_MESSAGE. Эта макрокоманда обслуживает сообщения операционной системы Windows, зарегистрированные с помощью функции RegisterWindowMessage. Параметра nMessageVariable этой макрокоманды указывает идентификатор сообщения, для которого будет вызываться метод memberFxn:
ON_REGISTERED_MESSAGE(nMessageVariable, memberFxn)
Макрокоманда ON_MESSAGE. Данная макрокоманда обрабатывает сообщения, определенные пользователем. Идентификатор сообщения (его имя) указывается параметром message. Метод, который вызывается для обработки сообщения, указывается параметром memberFxn:
ON_MESSAGE(message,memberFxn)
Макрокоманда ON_COMMAND. Эти макрокоманды предназначены для обработки командных сообщений. Командные сообщения поступают от меню, кнопок панели управления и клавиш акселераторов. Характерной особенностью командных сообщений является то, что с ними связан идентификатор сообщения.
Макрокоманда ON_COMMAND имеет два параметра. Первый параметр соответствует идентификатору командного сообщения, а второй - имени метода, предназначенного для обработки этого сообщения. Таблица сообщений должна содержать не больше одной макрокоманды для командного сообщения:
ON_COMMAND(id,memberFxn)
Обычно командные сообщения не имеют обработчиков, используемых по умолчанию. Существует только небольшая группа стандартных командных сообщений, имеющих методы-обработчики, вызываемые по умолчанию. Эти сообщения соответствуют стандартным строкам меню приложение. Так например, если строке Open меню File присвоить идентификатор ID_FILE_OPEN, то для его обработки будет вызван метод OnFileOpen, определенный в классе CWinApp.
Макрокоманда ON_COMMAND_RANGE. Макрокоманда ON_COMMAND ставит в соответствие одному командному сообщению один метод-обработчик В некоторых случаях более удобно, когда один и тот же метод-обработчик применяется для обработки сразу нескольких командных сообщений с различными идентификаторами. Для этого предназначена макрокоманда ON_COMMAND_RANGE.
Она назначает один метод-обработчик для обработки нескольких командных сообщений, интервалы которых лежат в интервале от id1 до id2:
ON_COMMAND_RANGE(id1,id2,memberFxn)
Макрокоманда ON_UPDATE_COMMAND_UI. Данная макрокоманда обрабатывает сообщения, предназначенные обновления пользовательского интерфейса, например меню, панелей управления, и позволяет менять их состояние.
Параметр id указывает идентификатор сообщения, а параметр memberFxn - имя метода для его обработки:
ON_UPDATE_COMMAND_UI(id,memberFxn)
Методы, предназначенные для обработки данного класса сообщений, должны быть определены с ключевым словом afx_msg и иметь один параметр - указатель на объект класса CCmdUI. Для удобства имена методов, предназначенных для обновления пользовательского интерфейса, начинаются с OnUpdate:
afx_msg void OnUpdate<имя_обработчика>(CCmdUI* pCmdUI);
В качестве параметра pCmdUI методу передается указатель на объект класса CCmdUI. В нем содержится информация об объекте пользовательского интерфейса, который нужно обновить, - строке меню или кнопке панели управления. Класс CCmdUI также включает методы, позволяющие изменить внешний вид представленного им объекта пользовательского интерфейса.
Сообщения, предназначенные для обновления пользовательского интерфейса, передаются, когда пользователь открывает меню приложения, а также во время цикла ожидания приложения, когда очередь сообщений приложения становится пуста.
При этом посылается несколько сообщений, по одному для каждой строке меню. С помощью макрокоманд ON_UPDATE_COMMAND_UI можно определить методы-обработчики, ответственные за обновление внешнего вида каждой строки меню и соответствующие ей кнопки на панели управления. Эти методы могут изменять состояние строки меню - отображать ее серым цветом, запрещать ее выбор, отображать около нее символ "галочка" и т.д.
Если не определить метод для обновления данной строки меню или кнопки панели управления, то выполняется обработка по умолчанию. Выполняется поиск обработчика соответствующего командного сообщения, и, если он не обнаружен, выбор строки запрещается.
Макрокоманда ON_UPDATE_COMMAND_UI_RANGE. Эта макрокоманда обеспечивает обработку сообщений, предназначенных для обновления пользовательского интерфейса, идентификаторы которых лежат в интервале от id1 до id2. Параметр memberFxn указывает метод, используемый для обработки:
ON_UPDATE_COMMAND_UI_RANGE(id1,id2,memberFxn)
Макрокоманда ON_<name>. Данные макрокоманды предназначены для обработки сообщений от органов управления. Такие сообщения могут передаваться органами управления диалоговой панели. Сообщения от органов управления не имеют обработчиков, используемых по умолчанию. При необходимости их нужно определить самостоятельно.
Все макрокоманды ON_<name> имеют два параметра. В первом параметре id указывается идентификатор органа управления. Сообщения от этого органа управления будут обрабатываться методом memberFxn. Например:
ON_BN_CLICKED(id,memberFxn)
Макрокоманда ON_CONTROL_RANGE. Эта макрокоманда обрабатывает сообщения от органов управления, идентификаторы которых находятся в интервале от id1 до id2. Параметр wNotifyCode содержит код извещения. Метод-обработчик указывается параметром memberFxn:
ON_CONTROL_RANGE(wNotifyCode,id1,id2,memberFxn)
Все объекты СОМ используют подсчет ссылок для определения момента, когда они могут безопасно себя разрушить (см. раздел "Подсчет ссылок"). Но в случае объекта, выполняющегося на удаленной машине, есть одна проблема: что будет, если исполнение клиента завершится внезапно? Когда объект и его клиент выполняются на одной машине, объект может быть уведомлен о внезапной смерти клиента непосредственно. Но в распределенном случае ситуация несколько сложнее.
К счастью, эта проблема давно решена: клиент может производить периодический тестовый опрос (pinging) каждого используемого им объекта путем выдачи ему вызова ORPC. Если в течение достаточно длительного времени от данного клиента не приходит подобный тест, клиент считается "погибшим", и объект может предпринять соответствующие действия. Тестовый опрос может служить простым способом поддержки подсчета ссылок в распределенной среде.
Но тестовый опрос может быть и невероятно неэффективным. Вообразите, например, что на машине работают 10 клиентов и у каждого из них по 30 указателей на интерфейсы 10 разных объектов, причем последние все расположены на второй машине. Наивный (и очень дорогой) метод тестового опроса может требовать от каждого клиента опрашивать все интерфейсы каждого объекта по отдельности. Разумнее, объединив эти опросы, обеспечить гораздо более эффективное использование сети.
Так и поступает DCOM. Вместо того, чтобы требовать от каждого клиента опрашивать все объекты индивидуально, пакеты тестового опроса отправляются и принимаются разрешателями OXID. Разрешатель OXID определяет, какие интерфейсы относятся к одному и тому же объекту и какие объекты находятся на одной машине. Затем он определяет набор тестового опроса (ping set), включающего в себя все интерфейсы этих объектов, обращаясь к IObjectExporter::ComplexPing разрешателя OXID удаленной машины. После этого, разрешатель OXID может выполнять один тестовый опрос другого разрешателя для всего набора с помощью IObjectExporter::SimplePing. Это гораздо эффективнее посылки пакета опроса каждым клиентом каждому объекту и уж тем более — тестового опроса каждого интерфейса каждого объекта.
В среде Visual C++ можно строить различные типы проектов. Такие проекты после их создания можно компилировать и запускать на исполнение. Фирма Microsoft разработала специальный инструментарий, облегчающий и ускоряющий создание проектов в среде Visual C++.
Рассмотрим некоторые типы проектов, которые можно создавать при помощи различных средств (мастеров проектов) Microsoft Visual C++:
MFC AppWizard (exe) – при помощи мастера приложений можно создать проект Windows-приложения которое имеет однодокументный, многодокументный или диалоговый интерфейс. Однодокументное приложеие может предоставлять пользователю в любой момент времени работать только с одним файлом. Многодокументное приложение, напротив, может одновременно представлять несколько документов, каждый в собственном окне. Пользовательский интерфейс диалогового приложения представляет собой единственное диалоговое окно.
MFC AppWizard (dll) – этот мастер приложений позволяет создать структуру DLL, основанную на MFC. При помощи него можно определить характеристики будующей DLL.
AppWizard ATL COM – это средство позволяет создать элемент управления ActiveX или сервер автоматизации, используя новую библиотеку шаблонов ActiveX (ActiveX Template Library - ATL). Опции этого мастера дают возможность выбрать активный сервер (DLL) или исполняемый внешний сервер (exe-файл).
Custom AppWizard – при помощи этого средства можно создать пользовательские мастера AppWizard. Пользовательский мастер может базироваться на стандартных мастерах для приложений MFC или DLL, а также на существующих проектах или содержать только определеямые разработчиком шаги.
DevStudio Add-in Wizard – мастер дополнений позволяет создавать дополнения к Visual Studio. Библиотека DLL расширений может поддерживать панели инструментов и реагировать на события Visual Studio.
MFC ActiveX ControlWizard - мастер элементов управления реализует процесс создания проекта, содержащего один или несколько элементов управления ActiveX, основанных на элементах управления MFC.
Win32 Application – этот мастер позволяет создать проект обычного Window-приложения. Проект создается незаполненным, файлы с исходным кодом в него следует добавлять вручную.
Win32 Console Application – мастер создания проекта консольного приложения. Консольная приложение – это программа, которая выполняется из командной cтроки окна DOS или Windows и не имеет графического интерфейса (окон). Проект консольного приложения создается пустым, предполагая добавление файлов исходного текста в него вручную.
Win32 Dynamic-Link Library – создание пустого проекта динамически подключаемой библиотеки. Установки компилятора и компоновщика будут настроены на создание DLL. Исходные файлы следует добавлять вручную.
Win32 Static Library – это средство создает пустой проект, предназначенный для генерации статической (объектной) библиотеки. Файлы с исходным кодом в него следует добавлять вручную.
С помощью событий элемент управления OLE сообщает контейнеру о том, что “что-то произошло”. Стандартными примерами событий являются ввод данных с клавиатуры, перемещения и щелчки мыши, а также изменение внутреннего состояния элемента управления. Уникальность событий заключается в том, что элемент управления определяет тип события, но обрабатывается оно контейнером.
Доступ к методам обработки событий осуществляется с помощью средств OLE-автоматизации. Это означает, что они: имеют идентификаторы (DISPID); могут иметь произвольное число параметров; могут возвращать определенные значения.
Элемент управления OLE использует события для оповещения о том, что произошло некоторое действие или выполнилось некоторое условие (в библиотеке типов элемента управления следует описать все события, которые может сгенерировать данный элемент). Элемент управления определяет, что именно произошло, и генерирует требуемое событие, а контейнер его распознает и соответствующим образом обрабатывает. Для этого в контейнере должен быть реализован соответствующий набор методов-обработчиков событий.
Для реализации событий элементы управления и их контейнеры используют так называемые точки соединения (connection point). При встраивании элемента управления в контейнер сканирует все точки соединения элемента управления, пытаясь найти ту, которая обозначена как интерфейс диспетчеризации событий IDispatch (эта информация хранится в odl-файле элемента управления). В случае успеха контейнер подключает к ней свой интерфейс IDispatch, устанавливая, таким образом, канал связи.
События можно разделить на несколько видов: базовые события (stock events); события-запросы (request events); события-предупреждения (before events); события-следствия (after events); события-директивы (do events). Независимо от вида, все события различаются по имени и идентификатору (DISPID). Для каждой группы событий действуют свои соглашения о правилах присвоения имен, последовательности вызовов и т.п. Однако, по существу, все события одинаковы.
Все они генерируются (или порождается) элементом управления и через точку соединения попадают в контейнер. Получив событие, контейнер реагирует на него по своему усмотрению.
Так же как и у базовых свойств, у базовых событий есть заранее определенные имена и идентификаторы (DISPID). Приведем список всех базовых событий (их смысл понятен из названия): Click, DblClick, KeyDown, KeyPress, KeyUp, MouseDown, MouseMove, MouseUp, Error. В классе COleControl для генерации событий существуют методы, предназначенные для генерации всех базовых событий, их имена начинаются с Fire (например, FireClick). Для генерации пользовательских событий применяется методв FireEvent. (Пользовательскими называются события, которые сам программист добавляет к элементу управления. Они отличаются от базовых свойств тем, что класс ColeControl не генерирует их автоматически и не имеет стандартных методов оповещения, которые позволяют отслеживать их возникновение).
В классе ColeControl имеется ряд методов оповещения, вызываемых библиотекой MFC при генерации базовых событий: OnClick, OnKeyDownEvent, OnKeyPressEvent, OnKeyUpEvent. Эти методы являются виртуальными, поэтому элемент управления также может перехватывать события и самостоятельно их обрабатывать.
Для того, чтобы дать контейнеру возможность отменить какое-нибудь действие, элемент управления может генерировать события-запросы. Например, если пользователь решил закрыть элемент управления, тот может выдать событие RequestClose. Это позволяет процедуре-обработчику, находящейся в контейнере, отменить закрытие. Иначе говоря, элемент управления запрашивает разрешение сделать что-то, а контейнер должен решить, выполнять этот запрос или нет.
По умолчанию, последним параметром методов, генерирующих события-запросы, является ссылочная переменная типа CancelBoolean (тип данных, определенный в OLE). Эта переменная традиционно имеет имя Cancel, а ее значение указывает, должен ли элемент управления отвергнуть запрошенное действие (Cancel=TRUE) или выполнить его (Cancel=FALSE).
Во время события-запроса происходит следующее. OLE-элемент- управления устанавливает значение переменной Cancel в FALSE, тем самым давая знать контейнеру, что он должен что-то сделать. Элемент управления генерирует событие, вызывая соответствующий метод автоматизации в формате Request<Action>, где <Action> - запрошенное действие. Контейнер, в зависимости от запроса и текущего состояния системы, устанавливает значение параметра Cancel в TRUE (заставляя элемент управления отменить действие) или FALSE (разрешая элементу управления выполнить действие). Когда метод, сгенерировавший событие, завершается, OLE-элемент управления проверяет значение параметра Cancel и предпринимает соответствующие действия.
Перед тем как сгенерировать “настоящее” событие, элемент управления может сгенерировать событие-предупреждение. Это действие называется упреждающим оповещением (prenotification). Оно дает контейнеру возможность подготовиться к тому, что должно произойти. Следует учитывать, что контейнер не может отменить событие-предупреждение. Получив его, он может лишь выполнить какие-нибудь действия.
В соответствии с соглашением, имена методов, генерирующих события-предупреждения, имеют следующий вид: Before<Action>, где <Action> - действие, которое собирается предпринять элемент управления OLE (например, метод BeforeClose()).
После того, как что-то произошло, элемент управления может сгенерировать событие-следствие. Это действие называется последующим оповещением (postnotification). По сравнению с другими типами событий события-следствия являются наиболее распространнеными. Типичными примерами таких событий являются щелчки мыши и изменение свойств. Имена методов, генерирующих события-следствия, не подчиняются соглашениям о правилах присвоения имен. Кроме того, события-следствия не могут отменяться контейнерами.
События-директивы определяются и реализуются в элементе управления, но могут быть переопределены в контейнере. Они генерируются для того, чтобы дать пользователю возможность переопределить или дополнить код обработчиков событий, реализованных в элементе управления по умолчанию.
Имена методов, генерирующих события-директивы, должны соответствовать следующему соглашению: Do<Action>. Последним параметром методов, генерирующих события-директивы, является булева переменная EnableDefault. Элемент управления устанавливает ее значение в TRUE и вызывает метод. Получив событие, контейнер может в ответ предпринять некоторые действия. Затем он устанавливает значение переменной EnableDefault в TRUE или FALSE, в зависимости от того, хочет ли он, чтобы элемент управления продолжил обработку события. Когда метод генерации сообщения завершается, элемент управления проверяет параметр EnableDefault. Если он равен TRUE, элемент управления вызывает стандартную реализацию метода, а если FALSE, то не вызывает ее.
Свойства (properties) определяют внешний вид и поведение элемента управления. У всех свойств есть следующие атрибуты:
Имя - это читабельное имя, доступное внешнему миру. Контроллеры автоматизации используют имена для доступа и их модификации.
Тип - у всех свойств есть связанные с ними типы данных. Так как свойства доступны внешнему миру посредством механизма OLE-автоматизации, то их типы ограничиваются набором типов OLE, рассматриваемых ниже.
Значение - каждое свойство имеет определенное значение заданного типа.
Идентификаторы диспетчеризации (DISPID) - у всех элементов автоматизации, то есть свойств, методов и событий, есть числовые идентификаторы (GUID), под которыми они известны системе. Об установлении соответствия между именами свойств и их DISPID заботится класс ColeControl, реализуя интерфейс IDispatch.
Символьная константа - обычно с идентификатором каждого свойства элемента управления связана символьная константа. Вместо доступа к свойствам по их именам и идентификаторам, клиенты могут использовать символьные константы.
Элемент управления может предоставлять свои свойства непосредственно по имени либо с помощью специальных методов чтения/записи, называемых методами свойств (properties methods).
С точки зрения реализации, свойства являются элементами класса C++, определенными в классе реализации элемента управления. Клиенты получают доступ к свойствам по имени, идентификатору DISPID или символьной константе. В элементе управления OLE свойство реализуется либо с использованием элемента класса и метода оповещения (notification method), либо с помощью пары методов - чтения/записи (методов Get/Set).
Свойства бывают следующих типов: базовые (stock), внешние (ambient), расширенные (extended) и пользовательские (custom).
Базовыми называются свойства, определенные фирмой Microsoft. Это набор характеристик, которые обычно свойственны всем элементам управления независимо от их типа и выполняемых функций (например, цвет фона). Следует отметить, что элемент управления не обязан поддерживать базовые свойства.
Любой элемент, который хочет реализовать базовые свойства, может использовать для доступа к ним стандартные имена и DISPID, определенные фирмой Microsoft, а также методы чтения/записи класса COleControl (например, GetBackColor и SetBackColor). Эти методы могут быть вызваны как из самого элемента управления, так из внешнего объекта с помощью средств автоматизации.
В классе COleControl есть несколько специальных методов, сообщающих элементу об изменении его базовых свойств (OnBackColorChanged, OnBorderStylerChanged, OnEnabledChanged, OnFontChanged,OnForeColorChanged, OnTextChanged). Данные методы объявлены виртуальными, поэтому их можно спокойно изменять. Это следует делать в том случае, если элементу необходимо отслеживать изменения базовых свойств и реагировать на них.
Внешние свойства представляют собой стандартный набор свойств, доступных только для чтения и реализованных во всех контейнерах. Они предоставляют элементу управления информацию о его окружении, т.е. о контейнере. Элементы управления могут использовать эту информацию для согласования своего визуального представления в соответствии с интерфейсом контейнера. Для чтения внешних свойств контейнера используются методы, реализованные в классе COleControl и имеющие префикс Ambient...(). Необходимо отметить, что внешние свойства реализуются контейнером, а элемент только считывает их. Нужно добавить, что контейнеры не обязаны реализовывать такие свойства.
Расширенными свойствами называются такие свойства, которые контейнер связывает с каждым конкретным элементом управления. Обратим внимание на то, что контейнер сам несет ответственность за эти свойства и “подключает” их к каждому элементу по своему усмотрению. Для того, чтобы контейнер мог связать расширенные свойства с элементом управления, он во время встраивания элемента создает небольшой вспомогательный объект, называемый расширенным элементом управления. Посредством механизма агрегации этот объект принимает все вызовы методов свойств элемента управления, что позволяет ему обрабатывать их прежде, чем они попадут к реальному элементу.По существу, расширенный элемент управления является внешней оболочкой элемента управления OLE.
Пользовательскими называются свойства, которые определены разработчиком OCX-объекта. Пользовательские свойства применяются для придания отличительных черт элементу управления. Каждому пользовательскому свойству необходимо присвоить уникальное имя и DISPID. Кроме того, следует убедиться что это свойство имеет один из типов OLE.
Обычно конечные пользователи не подозревают о том, что они работают с управляющим элементом. Вместо этого они видят графический пользовательский интерфейс: кнопки, которые можно нажимать, ползунки, которые можно двигать, поля, куда можно вводить текст, и т.д. Большинство современных операционных систем позволяют приложениям представить подобный интерфейс.
Все чаще и чаще то, что пользователь видит единым целым, на самом деле является контейнером с управляющими элементами ActiveX. Контейнер управляющих элементов подобен контейнеру составных документов OLE, но поддерживает несколько дополнительных интерфейсов для работы с управляющими элементами ActiveX. Каждый управляющий элемент подключен к контейнеру и обычно представляет свой собственный пользовательский интерфейс как внедренный объект, поддерживающий активизацию "на месте". Например, кнопка на экране может быть пользовательским интерфейсом некоего управляющего элемента ActiveX. Щелкая ее и взаимодействуя с исполняющимся в результате кодом, пользователь фактически активизирует элемент управления ActiveX. To, что пользователь видит как одно приложение, представляющее один интегрированный пользовательский интерфейс, на самом деле — контейнер управляющих элементов, полный различных дискретных управляющих элементов ActiveX, каждый из которых выполняет часть общей работы.
Конечный пользователь видит одно приложение, но кто-то должен был это приложение создать, подключив к контейнеру управляющий элемент. Построение приложения из управляющих элементов заметно отличается от создания приложения с нуля. Применение управляющих элементов значительно ускоряет процесс — в конце концов построить дом из готовых блоков быстрее, чем сначала сделать блоки, а уж затем строить из них дом. Кроме того, использование управляющих элементов проще и требуе от программиста меньшей квалификации. Такова цель компонентного программного обеспечения: ускорение и упрощение разработки приложений.
Чтобы при создании приложения использовать управляющие элементы, разработчик вначале должен принять решение относительно их контейнера. В числе популярных инструментов создания контейнеров Microsoft Visual Basic и Developer Studio и множество других средств третьих фирм. Кроме того, в качестве контейнера можно использовать средство просмотра WWW, при этом управляющий элемент должен быть встроен в HTML-файл.
Затем следует решить, какие управлющие элементы ActiveX включить для обеспечения предполгаемых функциональных возможностей. В состав Visual Basic и Developer Studio входит множество управляющих элементов, так что программист без труда найдет нужный. Если нет, то проблему решит большой и быстро растущий рынок управляющих элементов ActiveX третьих фирм, где представлена продукция сотен компаний.
Если подходящего управляющего элемента нет ни в Visual Basic, ни на рынке третьих фирм, его можно разработать самостоятельно. Для этого необходима совершенно иная квалификация, чем для создания приложения, использующего управляющий элемент. В среде, где в основе создания программ лежит применение компонентов, более вероятно разделение программистов на две группы: одна сосредоточится на разработке специализированных управляющих элементов ActiveX, а другая займется сборкой из них законченных приложений. Эти две группы — их иногда называют создателями (creators) и сборщиками (assemblers) — выполняют взаимодополняющие функции в разработке программного обеспечения на основе компонентов.
То, как выглядит управляющий элемент для своего создателя, зависит от используемого инструментария. И контейнер, и управляющие элементы, содержащиеся в нем, реализуют СОМ-объекты, каждый из которых поддерживает определенный набор интерфейсов. Этот набор может быть весьма большим, и каждый интерфейс может содержать много методов. Очевидно, что написать новый управляющий элемент "с нуля" не так-то просто.
Простой управляющий элемент, поддерживающий ограниченный набор возможностей, лучше всего, вероятно, разработать целиком вручную — так можно получить более быстрый и компактный код. Но реализация даже весьма сложного управляющего элемента может оказаться простой, если в распоряжении программиста есть мощный набор инструментов. Например, в состав Microsoft Developer Studio входит Control Development Kit (CDK — набор инструментов для разработки управляющих элементов) и ControlWizard (мастер управляющих элементов), сочетание которых позволяет опытным программистам на C++ разрабатывать управляющие элементы, даже не зная большей части деталей реализации управляющих элементов.
Может показаться, что понять, как работают управляющие элементы, сложно, но эта сложность исчезает при систематическом подходе. Функциональность, определяемая спецификацией управляющих элементов ActiveX, распадается на 4 основных аспекта, для реализации каждого предназначена особая группа интерфейсов:
обеспечение пользовательского интерфейса;
обеспечение вызова методов управляющего элемента контейнером;
посылка событий контейнеру;
получение информации о свойствах среды контейнера и обеспечение доступа к свойствам управляющего элемента и их модификации.
С каждым потоком связана определенная установка приоритета. Эта установка представляет собой комбинацию двух значений: значения общего класса приоритета процесса и значения приоритета самого потока относительно данного класса. Фактический приоритет потока определяется путем сложения класса приоритета процесса и уровня приоритета самого потока.
Приоритет потока показывает, сколько времени работы процессора требуется потоку. Для потоков с низким приоритетом требуется мало времени, а для потоков с приоритетом - много времени. Нужно заметить, что конечно же, количество времени, которое занимает поток у процессора, существенным образом влияет на характеристики выполнения потока и его взаимодействие с другими, выполняющимися в данный момент потоками.
Получить класс приоритета процесса можно с помощью функции GetPriorityClass, а установить класс приоритета можно с помощью функции SetPriorityClass. Обе эти функции являются API-функциями и не входят в класс CWinThread.
Ниже показаны константы, соответсвующие классам приоритетов в порядке убывания:
REALTIME_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
NORMAL_PRIORITY_CLASS
IDLE_PRIORITY_CLASS
По умолчанию программе присваивается приоритет NORMAL_PRIORITY_CLASS. Как правило, причин менять его нет. Фактически, изменение приоритета процесса может негативно сказаться на производительности всей системы. Так например, увеличение класса приоритета программы до REALTIME_PRIORITY_CLASS приведет к захвату программой всех ресурсов процессора.
Приоритет процесса (независимо от класса приоритета) говорит о том, сколько времени процессора занимает отдельный поток в пределах своего процесса. При создании потока ему присваивается нормальный приоритет. Но это значение можно изменить, причем даже во время выполнения потока.
Приоритеты потоков контролируются методами класса CWinThread. Определить значение приоритета можно с помощью метода GetThreadPriority, а изменить его - с помощью метода SetThreadPriority
Ниже приведены константы, соответствующие установкам приоритетов в порядке убывания:
THREAD_PRIORITY_TIME_CRITICAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_NORMAL
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_IDLE
Благодаря различным сочетаниям значений приоритета процесса и приоритета потока в Win32 поддерживается 31 различная установка приоритета.
Почему для включения в текстовый документ электронной таблицы необходимо использовать весь Excel? Если нужны только основные возможности электронных таблиц, то вероятно, можно обойтись меньшим, более быстрым и дешевым компонентом. Многим программистам пришлась бы по вкусу возможность построить целое приложение по большей части из готовых компонентов.
Именно подобное желание и привело к идее компонентного программного обеспечения — области, где СОМ способен на очень многое. Повторно применимые компоненты можно создавать на основе исключительно самой СОМ, но для этой цели полезно определить и некоторые стандартные интерфейсы, и соглашения. Используя последние, можно создавать компоненты, единообразно выполняющие такие распространенные задачи, как обеспечение пользовательского интерфейса и посылка сообщений клиенту. Эти стандарты и определяет спецификация управляющих элементов ActiveX (ActiveX Controls).
Управляющий элемент ActiveX — независимый программный компонент, выполняющий специфические задачи стандартным способом. Разработчики могут задействовать один или несколько таких элементов в приложении, чтобы получить преимущества функциональных возможностей существующего программного обеспечения. В результате программное обеспечение построено в основном из уже готовых частей — будет то, что называется компонентным программным обеспечением.
Первоначально управляющие элементы ActiveX были известны под названием управляющие элементы OLE или ОСХ. Microsoft изменила название, чтобы отразить некоторые новые возможности, сделавшие эти элементы более подходящими для Интернета и WWW. Например, управляющий элемент ActiveX может хранить свои данные на странице где-то в WWW либо может быть выкачан с сервера WWW и затем запущен на машине клиента. И контейнер, в котором работает управляющий элемент, не обязан быть средой программирования — вместо этого он может быть средством просмотра WWW.
Управляющие элементы ActiveX — не отдельные приложения. Напротив, они являются серверами, которые подключаются к контейнеру элементов. Как обычно, взаимодействие между управляющим элементом и его контейнером определяется различными интерфейсами, поддерживаемыми СОМ-объектами. Фактически управляющие элементы ActiveX используют многие другие технологии OLE и ActiveX. Например, управляющие элементы обычно поддерживают интерфейсы для внедрения и зачастую предоставляют доступ к своим методам через диспинтерфейсы Автоматизации.
Одна из главных целей СОМ — обеспечить производство компонентных программ, т.е приложений, которые собираются из готовых элементов. Создавать работоспособные компоненты можно, используя лишь базовые соглашения, определенные СОМ. Все, что нужно, — определить и реализовать интерфейсы, соответствующие назначению данного компонента.
Большинство действительно полезных компонентов достаточно сложны. Слишком мало предоставить только несколько базовых методов, которые способны вызывать клиенты. Компоненты должны обеспечивать гораздо более разнообразные формы взаимодействия со своими клиентами. Например, почему бы не определить стандартный способ отображения компонентами собственного пользовательского интерфейса? Компонентам может потребоваться и механизм для посылки событий своим клиентам или механизм, позволяющий клиенту читать и изменять свойства компонента. Учитывая, что у многих компонентов будут, вероятно, схожие потребности в плане взаимодействия с клиентами, стоит определить общий способ удовлетворения этих потребностей. Подобные стандарты значительно облегчат жизнь и программному обеспечению, использующему компоненты. Вместо того, чтобы разбираться в особенностях большого числа разных компонентов, такое программное обеспечение могло бы поддерживать единый набор стандартов, которому следуют все компоненты.
Определение стандартов для программных компонентов и является задачей спецификации управляющих элементов ActiveX (ActiveX Controls). Путем установления стандартных интерфейсов, способных поддерживать СОМ-объекты для выполнения определенных действий, спецификация управляющих элементов ActiveX предоставляет общую схему построения мощных компонентов. А так как компонентам нужен способ эффективного взаимодействия с использующим их кодом, то спецификация управляющих элементов ActiveX определяет и правила создания контейнеров управляющих элементов (control containers) — клиентских программ, знающих как работать с этими элементами.
Управляющие элементы ActiveX могут быть весьма сложны. Чтобы понять, что они из себя представляют и как работают, стоит рассмотреть их с точки зрения трех групп: конечных пользователей, разработчиков приложений, которые применяют управляющие элементы ActiveX, и с точки зрения создателей управляющих элементов.
Попросту говоря, сейчас необходимо выполнить всю основную работу. Для этого следует:
выбрать команду “New ATL Object” меню “Insert” среды Developer Studio;
в окне мастера ATL Object Wizard следует выбрать категорию “Objects” и элемент “Simple Object”, затем нажать кнопку “Next”.
в поле ”Short Name” закладки “Names” появившегося диалогового окна “ATL Object Wizard Properties” необходимо указать имя объекта SimpleATL;
перейти к закладке “Attributes” этого же диалогового окна и выбрать свойства: “Threading model” – “Free”, “Interface” – “Dual”, “Aggregation” – “No”. Также следует отметить (выбрать) опции “Support ISupportErrorInfo”, “Support Connection Points”, “Free Threaded Marshaler”, а затем щелкнуть на кнопке ОК.
Теперь в рабочем окне классов проекта повляется два новых узла. Первый узел – это класс CSimpleATL, основанный на нескольких различных интерфейсах. Второй узел – это непосредственно интерфейс ISimpleATL, основанный на интерфейсе IDispatch. Мастер ATL Object Wizard генерирует класс CSimpleATL и файл IDL для данного проекта. Этот файл используется для создания библиотеки типов для нескольких интерфейсов COM.
Следует обратить внимание, что интерфейс ISimpleATL представлен в рабочем окне проекта в виде одного из подузлов. Это показывает, что класс CSimpleATL реализует по крайней мере интерфейс ISimpleATL.
Теперь необходимо добавить методы и свойства указанному интерфейсу. Для этого необходимо выполнить следующие действия:
выбрать узел ISimpleATL и щелкнуть на нем правой кнопкой мыши;
выбрать команду “Add Method” или “AddProperty”;
в случае добавления метода в поле “Method Name” ввести имя метода, а в поле “Parameters” ввести параметры, их типы и характеристики (как в описании методов IDL-файле). Реализации методов ATL должны возвращать HRESULT для всех методов и свойств. Характеристика [in] соответствует входным параметрам, а обозначение [out] — выходным, обозначение [out, retval] соответствует возвращаемому параметру (он должен быть только один, причем последним в списке параметров);
в случае добавления свойства следует ввести тип свойства и его имя, а также вариант реализации нового параметра (например, через пару функций Get/Put).
Как и раньше, следует откомпилировать сервер и зарегистрировать сервер автоматизации в операционной системе. Для регистрации DLL-сервера следует выбрать команду “RegisterControl” меню “Tools”. Локальный сервер регистрируется при его запуске с параметром /RegServer (наприме, SvrExe.exe /RegServer). В этом случае сервер регистрируется и на этом прекращает свою работу. Заметим, что для удаления информации о сервере из реестра Windows используется запуск сервера с ключом /UnregServer (команда SvrExe.exe /UnregServer).
Разработав программу внутреннего сервера обобщенного типа, для реализации предоставляемых сервером COM-объектов можно приступать к дополнению его классами MFC. Чтобы создать класс для организации взаимодействия с механизмом, можно использовать любой класс, производный от CCmdTarget библиотеки MFC (CWnd и многие другие).
Итак, чтобы сделать сервер более практичным, необходимо создать новый класс на основе CCmdTarget — основного класса автоматизации в библиотеке MFC. Для этого следует выполнить следующие действия:
выберите команду “New Class” мастера ClassWizard;
в поле “Name” ввести имя класса, например CSimpleMFC;
в поле “Base Class” указать CCmdTarget;
в разделе “Automation” указать “Createable by type ID” и убедиться, что в поле введен текст, например SvrDll.SimpleMFC.
Остановимся подробнее на последнем пункте. Для того чтобы новый класс работал с механизмом автоматизации, следует выбрать один из следующих двух вариантов раздела “Automation”:
Automation, предоставляющий таблицу свойств и методов; или
Createable by type ID (Создаваемый по идентификатору типа объекта), который, помимо этого, присваивает некоторое имя и параметр GUID для идентификации объекта в системном реестре.
Вариант Createable следует использовать, когда необходимо предусмотреть возможность создания объекта автоматизации внешними средствами, например с помощью функции CreateObject из арсенала Visual Basic. Вариант Automation пригодится лишь в случае, когда классы, обладающие средствами работы с механизмом автоматизации, будут создаваться только через другой аналогичный объект - например как составная часть иерархии объектов.
Выбрав вариант Createable, следует ввести псевдоним (programmatic ID) для своего объекта - т. е. наименование, по которому его можно будет найти в системном реестре Этот параметр всегда строится по следующему принципу: ИмяМодуля.ИмяОбъекта[.номер_версии]. Рекомендуется PROGID сделать максимально информативным.
Созданный мастером ClassWizard класс CSimpleMFC и является С++ -классом COM-объекта SvrDll.SimpleMFC, реализующим его интерфейс IID_ISimpleMFC (он наследуется от интерфейса IDispatch).
При включении (containment), которое также называют делегированием (delegation), внешний объект выступает как обычный клиент внутреннего. Внешний объект вызывает методы внутреннего объекта для выполнения своих собственных функций, однако эти методы остаются недоступными клиенту внешнего объекта непосредственно. Вместо этого, когда клиент вызывает метод одного из интерфейсов внешнего объекта, исполнение данного метода может включать в себя вызов некоторого метода какого-либо из интерфейсов внутреннего. Другими словами, интерфейс внешнего объекта содержит методы, вызывающие методы внутреннего.
Реализация включения столь же проста, как и реализация клиента, использующего любой объект СОМ. Внутренний объект не должен быть для этого написан как-то по-особому и фактически даже не в состоянии различить включение и прямое использование его клиентом. (С точки зрения внутреннего объекта, внешний — обычный клиент.) Благодаря своей простоте, включение является очень широко распространенным механизмом повторного применения в СОМ.
Серверы ActiveX делятся на три типа. Рассмотрим их предназначение.
Первым является полный сервер (full server) — неудачное название, поскольку можно подумать, что речь идет о полнофункциональном сервере ActiveX. На самом деле это не так. Прилагательное полный означает в данном случае, что он может выполняться и как сервер, и как полноценное приложение. Например, Microsoft Word можно запустить в виде самостоятельного приложения, создать документ или брошюру и затем сохранить содержание в виде doc-файла. Или же можно запустить другое приложение, например WordPad, и включить в него содержание, созданное в Microsoft Word.
Второй тип серверов является противоположностью первому и тоже назван неудачно — мини-сервер (mini-server). Это название означает, что сервер может использоваться только для включения его содержимого в другие приложения. Например, с Microsoft Word поставляется множество аплетов, помогающих создать документы профессионального вида. Одно из них называется WordArt. Если попытаться запустить эту программу, то выдается сообщение о том, что это приложение может выполняться только при запуске из другого приложения.
Поговорим теперь о третьем типе серверов. Здесь начинаются сложности. Третий тип серверов — серверы автоматизации (automation servers) — не имеет никакого отношения к полным или мини-серверам. Фактически такой сервер не позволяет включать свое содержимое в приложение, которое его выполняет. Вместо этого сервер автоматизации чаще всего просто предоставляет специальные объекты, методы и свойства, позволяющие управлять этим сервером. Предположим, например, что требуется объединить основной документ, созданный в Microsoft Word с документом-источником, хранящемся в базе данных Microsoft Access. Естественно, можно запустить Microsoft Word и выбрать в меню команду Слияние (наряду с десятком других) и достичь своей цели. Или же можно написать приложение на Visual Basic (или любом другом языке, поддерживающем автоматизацию) и удаленно контролировать Word. Таким образом можно настроить программу на автоматическое выполнение каждый день в 16:00 без вмешательства пользователя.
Существует два типа серверов автоматизации: серверы процесса (in-process servers) и локальные серверы (out-of-process servers). Коротко говоря, серверы процесса создаются на основании класса, хранящегося в файле DLL, загружаемого и выполняемого в том же адресном пространстве, что и само приложение. Все экземпляры класса имеют один и тот же код, но каждый имеет собственную область данных.
Локальные серверы работают в своем адресном пространстве. Этот тип серверов выполняется в виде exe-файлов (например, Word или Excel), которые могут либо управлять несколькими собственным экземплярами, либо запускать новую копию каждый раз при создании объекта сервера.
Логически облики привязаны к документу. У классов CDocument и CView есть механизмы для поддержания этой связи. Класс CDocument инкапсулирует список, хранящий все облики данного документа. С помощью методов этого класса облик добавляется и удаляется из списка, осуществляется просмотр списка и получение обликов.
Метод класса CView позволяет облику получить документ, за которым он закреплен:
CDocument* GetDocument() const;
Этот метод очень важен: без него облик не получит доступ к методам документа. Он возвращает константный указатель на документ, за которым закреплен облик. Если облик не закреплен ни за каким документом, возвращается NULL.
Допустим, документ имеет несколько обликов, один из которых изменяет документ. Тогда остальные облики должны синхронизировать свое отображение с измененным содержанием этого документа. Такую синхронизацию обеспечивает метод UpdateAllViews класса CDocument.
Классы объектов-обликов должны быть устроены так, чтобы при получении сообщения Update обрабатывающий их метод производил синхронизацию облика с документом. Обычно это означает синхронизацию данных облика и синхронизацию изображения в окне облика. Простейший вариант такой синхронизации дает метод OnUpdate класса CView. Он состоит в отправке объектом сообщения OnDraw самому себе. Если программист не предусмотрел своего метода обработки сообщения Update, то при получении этого сообщения облик выполняет метод OnUpdate базового класса CView.
Начиная с версии 4.0 MFC позволяет динамически загружать и выгружать DLL, в том числе и расширения. Для корректного выполнения этих операций над создаваемой DLL в ее функцию DllMain в момент отключения от процесса необходимо добавить вызов AfxTermExtensionModule. Последней функции в качестве параметра передается уже использовавшаяся выше структура AFX_EXTENSION_MODULE. Для этого в текст DllMain нужно добавить следующие строки.
if(dwReason == DLL_PROCESS_DETACH) { AfxTermExtensionModule(MyExtDLL); }
Кроме того, следует помнить, что новая библиотека DLL является динамическим расширением и должна загружаться и выгружаться динамически, с помощью функций AfxLoadLibrary и AfxFreeLibrary,а не LoadLibrary и FreeLibrary.
При запуске приложение пытается найти все файлы DLL, неявно подключенные к приложению, и поместить их в область оперативной памяти, занимаемую данным процессом. Поиск файлов DLL операционной системой осуществляется в следующей последовательности.
Каталог, в котором находится ЕХЕ-файл.
Текущий каталог процесса.
Системный каталог Windows.
Если библиотека DLL не обнаружена, приложение выводит диалоговое окно с сообщением о ее отсутствии и путях, по которым осуществлялся поиск. Затем процесс отключается.
Если нужная библиотека найдена, она помещается в оперативную память процесса, где и остается до его окончания. Теперь приложение может обращаться к функциям, содержащимся в DLL.
Объекты класса CView имеют окно, представляющее собой обычную прямоугольную область экрана, без рамки, меню и других элементов. Вывод в такое окно (в том числе и текста) производится в графическом виде. При работе с этим классом очень важны моменты, рассматриваемые ниже.
Сообщение и метод OnDraw
Предположим, в окне отображен какой-либо рисунок, который затем перекрыт другим окном, а через некоторое время верхнее окно сдвигается с рисунка. Если перекрывающее окно небольшое, например, окно меню, то при его закрытии перекрытая часть восстанавливается системой Windows. В большинстве же случаев Windows обращается за помощью к владеющему окном приложению и посылает ему сообщение о необходимости восстановить окно. Дело разработчика приложения, реагировать на это сообщение или нет. Если обработка сообщения не предусмотрена, то велика вероятность исчезновения части рисунка из окна.
Итак, для обеспечения корректного отображения информации в окне нужна функция, которая будет перерисовывать содержимое окна всякий раз, когда оно потребуется. Для этого служит метод OnDraw. В качестве параметра этому методу передается указатель на контекст устройства, используя который, можно перерисовать окно.
Графическое устройство и его контекст
Работа с графическими устройствами, такими, как принтер, плоттер, дисплей в системе Windows вообще и в Visual C++ в частности является аппаратно-независимой. Это значит, что при программировании под Windows средств прямого доступа к аппаратуре нет. Все взаимодействие с ней производится через функции API (Application Program Interface). При этом для вывода на графические устройства используется один и тот же набор функций.
Для того, чтобы определить, на какое устройство осуществляется вывод, в Windows и в Visual C++ используется понятие контекста устройства (device context). Далее везде будет рассматриваться контекст устройства, реализованный в Visual C++. Контекст устройства - это объект класса CDC, содержащий все методы для построения изображения в окне.
Кроме того, он содержит данные о графическом устройстве вывода. Для осуществления вывода создается контекст устройства и тем самым определяется конкретное устройство для вывода. А далее к созданному объекту можно применять все имеющиеся методы класса CDC.
При выводе многие параметры долгое время остаются неизменными, например, цвет линии и другие. Указывать все такие параметры при каждом обращении к методам вывода неудобно. Контекст устройства содержит целый ряд таких параметров, обычно их называют атрибутами контекста устройства. Методы имеют лишь те параметры, что определяют существо их действия, а остальные атрибуты для рисования берутся из контекста устройства. При создании контекста устройства его атрибуты устанавливаются по умолчанию. Затем их можно изменять методами класса CDC.
Поврежденная область и поврежденный прямоугольник
При работе с окнами обычно "повреждается" только часть окна, так что перерисовывать все окно неэкономно. Поэтому система Windows фиксирует не только необходимость перерисовки, но и информацию о поврежденной области (invalid region). Но более важным является понятие поврежденный прямоугольник (invalid rectangle) - минимальный прямоугольник, покрывающий поврежденную область. Windows и Visual C++ обеспечивают следующие возможности при работе с поврежденным прямоугольником:
Методы GetUpdateRect и GetUpdateRgn класса CWnd позволяют получить описание поврежденного прямоугольника и поврежденной области.
Если производить перерисовку стандартным путем (например, внутри метода обработки сообщения OnDraw), то рисование в окне результативно только в области поврежденного прямоугольника. В этом случае говорят, что поврежденный прямоугольник задает область усечения, вне которой содержимое окна не изменяется.
Если в момент возникновения поврежденной области сформированное ранее системой Windows сообщение WM_PAINT о необходимости перерисовки окна не было обработано приложением и стоит в очереди приложения, новое сообщение WM_PAINT в очередь не добавляется.В качестве поврежденной области берется минимальный прямоугольник, покрывающий одновременно старый и новый прямоугольники.
Методы Invalidate, InvalidateRect и InvalidateRgn класса CWnd позволяют объявить соответственно клиентскую область, некоторые прямоугольник и область окна поврежденными и послать сообщение WM_PAINT в очередь приложения.
Методы ValidateRect и ValidateRgn класса CWnd позволяют отменить объявление некоторого прямоугольника или области поврежденными. Это ведет к корректировке текущего поврежденного прямоугольника.
При создании окна поврежденный прямоугольник устанавливается равным клиентской части окна. Обработчик сообщения Update класса CView также устанавливает поврежденный прямоугольник равным клиентской части окна.
Параметры реестра машины в точности определяют, кто имеет право запуска серверов на данном компьютере. Более общая установка разрешает или запрещает удаленную активизацию вообще. Если она отключена, ни один удаленный клиент не сможет запускать серверы или подсоединяться к какому-либо объекту на данной машине. Возможно также определение защиты активизации на уровне класса, что позволяет контролировать, какие удаленные клиенты имеют право на запуск сервера некоторого класса. Перечень имеющих разрешение на это содержит список управления доступом (access control list — ACL). И наконец, к классам, для которых не установлена защита активизации на уровне класса, может применяться защита активизации по умолчанию. Как и при защите активизации на уровне класса, защита активизации по умолчанию определяет гех, кто имеет право на запуск сервера в данной системе, с помощью ACL.
Всегда, когда используется ACL, необходимо определить личность пользователя или объекта, выполняющего запрос. Но это приводит к другому вопросу: что такое личность объекта? Или что на жаргоне контроля прав доступа означает слово принципал (principal)? По сути, принципал — это некто (например, пользователь) или нечто (например, выполняющийся процесс), имеющий (-ее) учетную запись (account) в данной среде независимо от его природы. Регистрируясь в системе, пользователь идентифицирует своего принципала, вводя свой идентификатор пользователя и пароль. Предположим, после регистрации пользователь запустил некоего клиента, который создал объект на другой машине. Кем является принципал этого объекта? Ответ на данный вопрос важен, так как принципал объекта может определять, что может делать этот объект.
DCOM предоставляет несколько ответов, зависящих от конфигурационной информации класса. Вновь создаваемый объект может быть сконфигурирован для исполнения как определенный принципал, аналогично сервису Microsoft Windows NT. Но он может выполняться как тот же принципал, что и создавший его клиент, или как принципал интерактивного пользователя, запустившего клиент (если они различаются). Если для данного класса в реестре ничего не задано, вновь созданному объекту по умолчанию присваивается принципал клиента, создавшего его.
После создания объект готов к приему вызовов своих методов от клиентов. Когда последние выполняются на других компьютерах, можно с пользой задействовать ряд сервисов контроля доступа. Вот самые важные:
аутентификация позволяет удостовериться, что пользователь именно тот, за кого себя выдает. С помощью аутентификации объект достоверно определяет личность клиента. Если поддерживается взаимная аутентификация, то и клиент может быть уверен в том, что сервер является именно тем за кого себя выдает;
авторизация позволяет определить, что клиент имеет npаво делать. Для принятия подобного решения объект может использовать любую схему. Например, в Microsoft Windows NT объект может воспользоваться встроенной поддержкой ACL;
целостность данных гарантирует, что принятые по сети данные не были изменены при пересылке. В отсутствие этого сервиса некто мог бы "выловить" пакет из сети, изменить его и послать первоначальному реципиенту, причем последний не заметил бы подмены;
секретность данных гарантирует, что пересылаемые и сети данные не будут прочитаны при передаче. Обычно для этого используется шифрование данных, что может существенно снизить производительность.
Сервисы защиты могут обеспечиваться различными механизмами. Например, механизмы защиты, включенные в состав Windows NT, поддерживают все 4 упомянутых сервиса. Альтернативой является Kerberos — система защиты, разработанная в Массачусетском технологическом институте (Massachusetts Institute of Technology). Kerberos обеспечивает аутентификацию, целостность и секретность данных, но не решает вопросы авторизации (но Kerberos поддерживает взаимную аутентификацию — средство, отсутствующее в составе механизмов распределенной защиты Windows NT). Другие механизмы защиты предоставляют другие комбинации сервисов защиты.
В связи с большим количеством механизмов, обеспечивающих защиту вызовов, архитекторы DCOM столкнулись с проблемой: какой из них поддерживать? Было решено не выбирать какой-то один, но определить общие интерфейсы, способные работать со многими из существующих вариантов. Тем не менее первая версия DCOM, выпущенная в 1996 году, поддерживает только механизмы защиты Windows NT.
В следующих версиях планируется включить поддержку Kerberos.
Независимо от используемого базового механизма, интерфейсы защиты вызовов DCOM определяют 2 основные возможности: автоматическую и поинтерфейсную защиты. В первом случае клиентский или серверный процесс устанавливает уровень защиты по умолчанию для всех вызовов этого процесса или вызовов, выполняемых им. Во втором — разные установки защиты можно задать для разных интерфейсов одного объекта или для разных объектов. Можно использовать и оба варианта одновременно, установив значения по умолчанию с помощью автоматической защиты и выполнив затем тонкую настройку, используя поинтерфейсную защиту.
Автоматическая защита
Будет ли процесс клиентом, выдающим запросы, или объектом, принимающим запросы, или и тем и другим, он может задать значения по умолчанию автоматической защиты вызовом CoInitializeSecurity. Вызов этой функции достаточно сложен, но позволяет процессу быстро и полностью задать свои требования по защите. Вот его наиболее интересные параметры:
ACL определяет, кто имеет право вызывать методы любого объекта данного процесса, а также кому это явно запрещено;
уровень аутентификации процесса определяет, как часто выполняется аутентификация (один раз при установке соединения, один раз для каждого вызова, или один раз для каждого пакета), а также должны ли использоваться средства обеспечения целостности и секретности данных. Вызовы, выполняемые данным процессом, будут исполняться на заданном уровне защиты, и все вызовы извне, использующие уровень ниже заданного, будут отвергаться;
список сервисов аутентификации вызовов извне.
Вызвав эту единственную функцию, процесс может установить значения широкого диапазона параметров защиты. Но, решив не прибегать к ней, процесс все равно наследует значения по умолчанию для данной системы. В защищенной зоне системного реестра администратор может задать общемашинные значения по умолчанию, применяемые ко всем процессам, не вызывающим CoInitializeSecurity самостоятельно.
Эти установки по умолчанию включают уровень аутентификации и ACL, управляющий тем, кто имеет доступ к процессу без собственного ACL.
Поинтерфейсная защита
Автоматическая защита — грубый инструмент. Она определяет один набор параметров защиты для всего, что делает процесс, будь он клиентом или сервером. Автоматическая защита, кроме того, проста в использовании, и для многих приложений — это именно то, что нужно. Но не каждый процесс может обойтись одной автоматической защитой. Например, клиент может пожелать использовать разные уровни защиты для вызовов разных объектов или даже разных интерфейсов одного объекта. Серверу, реализующему несколько объектов, может потребоваться применять к каждому из них или к каждому из их интерфейсов особые схемы авторизации. Поинтерфейсная защита обеспечивает поддержку подобных тонких разграничении.
В отличие от автоматической защиты, когда один вызов устанавливает параметры как серверной, так и клиентской защиты, Поинтерфейсная защита выглядит для клиента и сервера по-разному. Чтобы использовать ее, клиент должен получить указатель на интерфейс IClientSecurity, тогда как серверу понадобится для этого указатель на интерфейс IServerSecurity. Методы этих интерфейсов позволяют клиенту и серверу определить конкретные нужные им сервисы и уровни защиты.
С точки зрения клиента, каждый объект поддерживает IClientSecurity. Клиент получает указатель на этот интерфейс обычным образом с помощью QueryInterface. На самом деле эта внешне очевидная ситуация не так проста. Фактически интерфейс IClientSecurity реализован самой СОМ в библиотеке, подгружаемой к клиенту. При запросе клиентом IClientSecurity с помощью QueryInterface обращение к объекту никогда не производится. Вместо этого возвращается указатель на локальную реализацию интерфейса.
С помощью методов полученного указателя IClientSecurity клиент может установить параметры защиты для каждого интерфейса объекта. В действительности, однако, при этом устанавливаются параметры, поддерживаемые заместителем данного интерфейса внутри клиента, так что вызовы IClientSecurity не поступают к самому объекту.
Среди методов IClientSecurity отметим:
SetBlanket устанавливает информацию защиты, используемую всеми вызовами через данный заместитель. Задаваемые здесь значения переопределяют те, что были заданы с помощью CoInitializeSecurity. SetBlanket можно использовать для указания сервиса аутентификации, такого как Kerberos (в будущем) или сервис аутентификации Windows NT (сегодня) со специфичным для данного сервиса именем клиента, передаваемым с каждым вызовом. Данный метод позволяет также установить уровень аутентификации, аналогично CoInitializeSecurity;
QueryBlanket возвращает текущие значения параметров защиты заместителя.
После того как клиент задал параметры защиты заместителя с помощью IClientSecurity::SetBlanket, эти параметры используются при всех вызовах, выполняемых через данный заместитель. Вызовы через другие заместители осуществляются через параметры, заданные для последних. (Если параметры защиты не были заданы явно, используются значения, установленные CoInitializeSecurity либо общемашинные умолчания.) Вспомогательные функции, в которых заключены типичные последовательности вызовов, несколько облегчают установку этих параметров. Вместо того, например, чтобы запрашивать IClientSecurity, вызывать IClientSecurity::SetBlanket и затем освобождать указатель на интерфейс IClientSecurity, клиент может вызвать CoSetProxyBlanket, помещающую все эти вызовы в "удобную упаковку".
Независимо от используемых параметров защиты каждый вызов, сделанный клиентом, приводит в конце концов к исполнению кода метода, реализованного объектом. Установление личности клиента (аутентификация) выполняется СОМ, а также любое необходимое декодирование и проверка целостности принятых данных. Если какая-то из проверок не удалась, вызов отвергается. А если все в порядке, метод объекта должен сам определить, имеет ли данный клиент право на выполнение того, что запрашивает. Другими словами, объект должен выполнить авторизацию.
Чтобы определить, что имеет право делать данный клиент, надо знать, кем он является и какие параметры защиты он задал для данного вызова. Для получения этой информации код метода объекта начинает с вызова библиотечной функции CoGetCallContext, возвращающей указатель на IServerSecurity — серверный аналог IClientSecurity. Задействовав методы этого интерфейса, объект может получить информацию о клиенте, сделавшем данный вызов, а затем использовать ее для определения того, что этот клиент имеет право делать.
Среди методов IServerSecurity отметим следующие:
QueryBlanket возвращает информацию, заданную клиентом с помощью IClientSecurity: :SetBlanket. Эта информация, включая имя клиента и запрошенный им уровень аутентификации, позволяет серверу принять решение по авторизации. Сервер может, например, убедиться в использовании клиентом надлежащего уровня аутентификации (чтобы это ни означало для данного объекта). Если проверка успешна, объект может далее сравнить имя клиента с ACL, управляющим доступом к объекту. Или же метод может использовать совершенно иную схему для принятия подобного решения — строго определенных правил здесь нет;
ImpersonateClient позволяет объекту или потоку управления внутри объекта "надеть" личину клиента. Возможны различные уровни имперсонации (управляются клиентом): от использования ее только для проверок ACL до способности объекта выполнять запросы к другим удаленным объектам под именем клиента — так называемого делегирования (delegation). (Впрочем, делегирование не поддерживается в первой версии DCOM);
RevertToSelf возвращает объекту или потоку внутри него первоначальную личность после вызова IServerSecurity::ImpersonateClient.
Как и в случае IClientSecurity, есть несколько вспомогательных функций. Например, объект может вызвать CoQueryClient-Blanket, заключающую в себе вызовы CoGetCallContext, IServerSecurity: :QueryBlanket и IServerSecurity:: Release.