Практика
MDI с поддержкой нескольких экранов

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика


В этой статье я хочу рассказать о небольшом приложении, которое было написано в тестовых целях году эдак, примерно, в 2007 - 2008 (точнее уже и не вспомню). Так уж сложилось, что на протяжении всей своей трудовой деятельности, связанной с Delphi, я сталкиваюсь с приложениями, разработанными по технологии MDI. И вот, в то далекое время, разжился я вторым монитором. Практически тут же в голове стала формироваться навязчивая идея: "MDI… Второй монитор… MDI… Два монитора… Все это просто необходимо подружить друг с другом!". Сказано – сделано, написал тестовый пример и… благополучно положил в дальний ящик на долгие годы. Периодически вспоминал, хотел оформить в виде статьи, но все руки не доходили (пример нужно было довести до ума, структурировать и т.п.) И вот свершилось, выкладываю.

Вообще, готовя эту статью, я подумал что сейчас (с высоты текущего опыта) я бы сделал пример несколько иначе. Но ведь программисты в большинстве своем народ ленивый (работает - не трогай!), и я не исключение, поэтому ничего переделывать не стал. Но все же несколько слов на эту тему хочется сказать. Один из классов, менеджер форм, реализован с применением шаблона проектирования "одиночка" (singleton), который гарантирует, что в однопроцессном приложении будет единственный экземпляр некоторого класса, предоставляющий глобальную точку доступа к этому экземпляру. Сейчас я бы его сделал иначе, а именно:

type
  TSingleton = class
  private
    class var Instance: TSingleton;
  public
    class function NewInstance: TObject; override;
  end;
   
class function TSingleton.NewInstance: TObject;
begin
   if Instance = nil then
      Instance := TSingleton(inherited NewInstance);
   Result := Instance;
end;

Идея, лежащая в основе этого кода, проста - переопределяется метод выделения памяти System.TObject.NewInstance, который в результате возвращает уже готовый объект, вместо того, чтобы заново выделять память. Instance — это классовое поле, имеющее одинаковое значение для всех экземпляров. В нем и хранится тот самый единственный экземпляр. Сколько бы раз мы не пытались создать объект, каждый раз будем получать один тот же объект.

К чему я об этом упомянул. Если решите применить этот подход в реальном приложении, лучше реализовать singleton наиболее правильным способом, то есть описанным выше.

Переходим к примеру. Структура проекта выглядит следующим образом:


В основе примера лежит менеджер форм (MDI.FormManager), интерфейс которого выглядит следующим образом:

type
  TChildMenuClick = procedure (Sender: TObject) of object;

  TFormManager = class
  strict private
    class var
      FInstance: TFormManager;
  private
    FForms: TList<TForm>;
    [Weak] FChildMenu: TMenuItem;
    FChildMenuClick: TChildMenuClick;
  public
    class function GetInstance: TFormManager;
    class procedure FreeInstance;

    procedure AddMainForm(Form: TForm);
    procedure DelMainForm(Form: TForm);
    function GetMainForm(hWhd: THandle): TForm;

    procedure RebuildMainMenu;
    procedure RebuildSysMenu; overload;
    procedure RebuildSysMenu(Sender: TForm); overload;

    property Forms: TList<TForm> read FForms;
    property ChildMenu: TMenuItem read FChildMenu write FChildMenu;
    property ChildMenuClick: TChildMenuClick read FChildMenuClick write FChildMenuClick;
  end;

implementation

{ TFormManager }

class function TFormManager.GetInstance: TFormManager;
begin
   if not Assigned(FInstance) then
   begin
      FInstance := TFormManager.Create;
      FInstance.FForms := TList<TForm>.Create;
   end;
   Result := FInstance;
end;

class procedure TFormManager.FreeInstance;
begin
   if Assigned(FInstance) and (FInstance.FForms.Count = 1) then
   begin
      FInstance.FForms.Free;
      FreeAndNil(FInstance);
   end;
end;

Два шаблона TemplateMainFrm и TemplateChildFrm сделаны для удобства. В них вынесен вспомогательный код, поэтому, унаследовав от них остальные формы, разработчику уже практически ни о чем не придется волноваться - все уже написано за него! Почему практически? Потому, что менеджер нужно немного поднастроить, и делается это в конструкторе основной формы.

procedure TMainForm.FormCreate(Sender: TObject);
begin
   TFormManager.GetInstance.ChildMenu := NChildForms;
   TFormManager.GetInstance.ChildMenuClick := NChildClick;
end;

Здесь мы указываем менеджеру какой, пункт меню нужно обновлять при создании/уничтожении дополнительных форм, и какой обработчик события будет у дочерних подпунктов. Затем отработает код родительского шаблона…

procedure TTemplateMainForm.FormShow(Sender: TObject);
begin
   TFormManager.GetInstance.AddMainForm(Self);
end;

…и форма будет зарегистрирована в менеджере (основная форма всегда находится в списке по индексу 0).

Определимся с терминологией. Так как в нашем приложении будет более одной MDIForm, будем называть главной ту, которая автоматически создается при запуске приложения, все остальные будем называть дополнительными. Такое деление не случайно, главная форма может (и, скорее всего, обязательно будет) отличаться от дополнительных, например наличием главного меню или тулбара.

Теперь научимся создавать дополнительные формы, благо это совсем не сложно. Вот как это делается:

procedure TMainForm.NAddMainFormClick(Sender: TObject);
const
  {$J+}
  FormIndex: Cardinal = 0;
  {$J-}
begin
   Inc(FormIndex);
   with TDuplicateMainForm.Create(Application.MainForm) do
   begin
      Caption := Format('Дополнительная форма [%d]', [FormIndex]);
      Show;
   end;
end;

Теперь, имея главное окно и несколько дополнительных, научимся создавать дочерние окна в любом из них.

procedure TMainForm.NChildClick(Sender: TObject);
var
  Frm: TForm;
begin
   // Создаем окно на Application.MainForm…
   ChildForm := TChildForm.Create(Application.MainForm);

   // …и при необходимости переносим на нужную форму
   if (Sender as TMenuItem).Tag > 0 then
   begin
      Frm := Application.MainForm;
      Pointer((@Application.MainForm)^) := TFormManager.GetInstance.Forms[(Sender as TMenuItem).Tag];
      ChildForm.ParentWindow := Application.MainForm.Handle;
      ChildForm.Perform(CM_RECREATEWND, 0, 0);
      Pointer((@Application.MainForm)^) := Frm;
   end
   else
      ChildForm.ParentWindow := Application.MainForm.Handle;

   TFormManager.GetInstance.RebuildSysMenu(ChildForm);
end;

Здесь используется хак, позволяющий получить адрес у свойства, если это свойство читает из поля напрямую, минуя геттер. Именно таким и является свойство MainForm у TApplication. Поэтому @Application.MainForm на самом деле даёт нам @Application.FMainForm - благодаря тому, что не имеет геттера.

Сначала я пытался создавать дочерние формы сразу в нужной, основной или дополнительной, форме (без переноса), но тогда у меня некорректно отрабатывал перенос дочерних форм в основную при закрытии дополнительной формы. При таком варианте никаких проблем нет.

Финальный штрих - перенос дочерней формы между главной/дополнительными (без этого все выше описанное теряет свой смысл).

// Код в шаблоне дочерней формы
procedure TTemplateChildForm.WndProc(var Msg: TMessage);
var
  Frm: TForm;
begin
   case Msg.Msg of
      WM_SYSCOMMAND: begin
         // При выборе системного меню формы мы получаем сообщение WM_SYSCOMMAND,
         // WParam которого содержит ID пункта меню, являющегося еще и индексом
         // формы, на которую нужно "перекинуть" дочернее окно
         if Msg.WParamLo <= 500 then
         begin
            if Msg.WParamLo > 0 then
            begin
               Frm := Application.MainForm;
               Pointer((@Application.MainForm)^) := TFormManager.GetInstance.Forms[Msg.WParamLo];
            end;

            ParentWindow := Application.MainForm.Handle;
            RecreateWnd;
            TFormManager.GetInstance.RebuildSysMenu(Self);

            if Msg.WParamLo > 0 then
               Pointer((@Application.MainForm)^) := Frm;
         end;
      end;
   end;

   inherited;
end;

Как это работает? Идентификаторы системного меню дочерней формы, отвечающие за ее перенос, соответствуют индексам форм в менеджере, на которые осуществляется перенос. При выборе такого пункта меню форма получает сообщение WM_SYSCOMMAND. Предполагаем, что у системного меню нет пунктов с такими маленькими ID, и что основных форм в приложении не будет слишком много (при желании их количество можно ограничить программно).

Еще нужно позаботиться о том, чтобы при уничтожении дополнительной формы все ее дочерние формы перемещались на основную:

// Код в шаблоне главной формы
procedure TTemplateMainForm.FormDestroy(Sender: TObject);
var
  i: Integer;
begin
   if Assigned(Application.MainForm) and (Self <> Application.MainForm) then
   begin // Закрытие дополнительой главной формы
      // Закрываем все дочерние формы
      for i := 0 to Application.MainForm.MDIChildCount-1 do
         if Self.MDIChildren[i].ParentWindow = Handle then
            Self.MDIChildren[i].Close;

      TFormManager.GetInstance.DelMainForm(Self);
   end;
end;

// Код в шаблоне дочерней формы
procedure TTemplateChildForm.FormClose(Sender: TObject;
  var Action: TCloseAction);
var
  i: Integer;
  Frm: TForm;
begin
   Frm := TFormManager.GetInstance.GetMainForm(ParentWindow);

   if not Assigned(Frm) then
      raise Exception.Create('Родительская форма не найдена.');

   if not (csDestroying in Frm.ComponentState) then
      // Если подьзователь сам закрывает доченюю форму, грохаем ее…
      Action := caFree
   else
   begin
      // …иначе (если закрывается родительская форма), переносим ее на главную
      Action := caNone;

      ParentWindow := Application.MainForm.Handle;
      RecreateWnd;
      TFormManager.GetInstance.RebuildSysMenu(Self);
   end;
end;

Перед уничтожением дополнительная форма закрывает все дочерние. В свою очередь дочерняя форма проверяет, как ее закрывают. Если родительская форма имеет статус csDestroying, значит она уничтожается и нужно перенестись на основную форму, в противном случае форму закрывает пользователь, подчиняемся и уничтожаемся.

Ну вот и все, вроде ничего сложного не было. Интересного Вам программирования!

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна