Глава 5. Повторное отображение графики


    В следующих трех шагах вы узнаете как

Шаг 7: Вывод на экран графики


    Возможно, вы удивитесь, узнав, что графика и текст, которые вы рисуете в окне с помощью функций Windows типа TextOut или LineTo, исчезают при изменении размера окна или повторного вывода его на экран. После того, как графические данные переданы Windows, через вызовы функций Windows вы никогда не получаете их обратно для повторного отображения.

    Чтобы в окне повторно отображалась графика, вы должны сохранить эту графику (или данные для регенерации графики) в структуре некоторого типа, такой как объект. С помощью объектов вы можете полиморфически хранить простую или сложную графику, а также хранить объекты и поля ваших оконных объектов.

Изображение и рисование


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

    Окна должны иметь возможность воссоздавать по запросу свои графические образы. Windows сообщает своим оконным объектам, когда они требуют изображения или обновления. При этом окно должно каким-то образом генерировать образ экрана. В ответ на необходимость изображения. ObjectWindows автоматически вызывает метод Paint вашего окна. Наследуемый и TWindow метод Paint не выполняет никаких функций. В Paint вы должны поместить код для передачи содержимого окна. Фактически Paint вызывается при первом выводе окна. Paint отвечает за обновление (при необходимости) изображения текущим содержимым.

    Существует еще одно важное отличие между отображением графики в методе Paint и другим ее отображением (например, в ответ на действия "мышью"). Содержимое экрана, которое должно использоваться для отображения, передается в параметре PaintDC, так что вашей программе не требуется получать или освобождать его. Однако вам потребуется вновь выбрать для PaintDC изобразительные средства.

    Чтобы отобразить содержимое окна, вместо повторения тех действий, которые привели к первоначальному изображению (DragDC), вы используете PaintDC. Визуальный эффект будет тот же, что и при первоначальном рисовании пользователем (аналогично проигрыванию аудиозаписи концерта). Но чтобы "проигрывать" ее в методе Paint, сначала вам нужно сохранить графику в виде объектов.

Сохранение графики в объектах


    Созданные программой Steps изображения на самом деле просто представляют наборы различного числа линий. При каждой буксировке "мыши" вы добавляете другую линию. А каждая линия - это на самом деле определенный набор точек, соединенных линейными сегментами. Чтобы сохранить и воспроизвести такие изображения, вам необходим гибкий и расширяемый тип данных

    Для размещения неизвестного числа линий или точек прекрасно подходит определенный в модуле Objects тип TCollection. Это набор объектов, который может динамически расширяться, когда вы добавляете другие элементы. Этим наборам не важно, что они включают, поэтому вы можете использовать один и тот же механизм и для рисунка (набора линий), и для линии (набора точек).

    Примечание: О наборах рассказывается в Главе 19 "Наборы".

    Концептуально вам нужно просто сделать так, чтобы окно знало о своем содержимом, так что оно сможет обновить изображение. Окно содержит рисунок, представляющий собой набор линий. Таким образом, вам нужно:

Добавление поля объекта


    Чтобы сохранить рисунок в виде набора линий, добавьте в TStepWindow поле с именем Drawing. В любой момент Drawing содержит текущий рисунок в виде набора объектов линий. Когда требуется отобразить окно, оно использует для изображения линии данные, записанные в Drawing.

Определение объекта линии


    Далее нужно ответить на вопрос, что такое линия. В шаге 4 вы видели, что изображаемая линия представляет собой просто набор точек, передаваемых из Windows в программу через сообщение wm_MouseMove. Для представления линий и точек вам необходимы объектные типы. Поскольку эффективный объект изображения линии должен быть повторно используемой частью, создайте отдельный модуль, определяющий объекты линий и точек.

    TLine содержит всю информацию, необходимую для изображения данной линии: перо и набор точек.

     type
       PLine = ^TLine;
       TLine = object(TObject)
           Points: PCollection;
           LinePen: PPen;
           constructor Init(APen: PPen);
           constructor Load(var S: TStream);
           destructor Done; virtual;
           procedure AddPoint(AX, AY: Word);
           procedure Draw(ADC: HDC);
           procedure Store(var S: TStream);
     end;

    LinePen просто указывает на объект TPen, а Point - это набор объектов точек. TLine и TLinePoint содержат методы Load и Store, преимущества использования которых для записи картинок на диск вы увидите в шаге 8. В отличие от них объект TLine весьма прост: конструктор и деструктор создают и уничтожают LinePen, AddPoint включает объект точки в Points, а Draw рисует линии между точками Points.

    Объект TLinePoint еще проще:

     type
       PLinePoint = ^TLinePoint;
       TLinePoint = object(TObject)
          X, Y: Integer;
          constructor Init(AX, AY: Integer);
          constructor Load(var S: TStream);
          procedure Store(var S: TStream);
     end;
     constructor TLinePoint.Init(AX, AY: Integer);
     begin
      X := AX;
      Y := AY;
     end;

    TLinePoint не определяет никакого нового поведения - это просто объект данных, который должен использоваться в TLine. Но позднее (в шаге 8) он понадобиться как объект для записи в поток. Не забудьте построить в TStepWindow.Init Drawing и уничтожить его в TStepWindow.Done:

     constructor TStepWindow.Init(AParent: PWindowsObject;
                                  ATitle: PChar);
     begin
       inherites Init(AParent, ATitle);
       ButtonDown := False;
       HasChanged := False;
       CommonPen := New(PPen, Init(ps_Solid, 1, 0));
       Drawing := New(PCollection, Init(50, 50));
     end;
     destructor TStepWindow.Done;
     begin
       Dispose(CommonPen, Done);
       Dispose(Drawing, Done);
       inherited Done;
     end;

    Основное окно программы Steps содержит набор в своем поле Drawing набор линий. Когда пользователь рисует линии, вы должны преобразовывать их в объекты и добавлять в Drawing. Затем, когда потребуется отобразить окно, путем итерации Drawing нужно отобразить каждую его точку.

Изменение методов работы с "мышью"


    Чтобы сохранять линии в виде объектов, вы должны изменить EMLButtonDown и WMMouseMove, чтобы не только рисовать линии, но также сохранять точки в наборе линий. Поскольку текущую линию придется обновлять не только одному методу, добавьте в TStepWindow еще одно поле типа PLine с именем CurrentLine:

     
     type
       TStepWindow = object(TWindow);
          CurrentLine: PLine;
            .
            .
            .
     end;

    Кроме добавления изображения линии WMLButttonDown создает при каждом вызове новый объект линии и добавляет его в набор в Drawing. WMMouseMove просто добавляет новую точку в конец объекта текущей линии и изображает в окне линейные сегменты. Сохраняя все точки всех линий, ваше окно будет записывать информацию, необходимую для точного воспроизведения картинки.

     procedure TStepWindow.WMLButtonDown(var Msg: TMessage);
     begin
       if not ButtonDown then
       begin
         ButtonDown := True;
         SetCapture(HWindow);
         DragDC := GetDC(HWindow);
         CommonPen^.Select(DragDC);
         MoveTo(DragDC, Msg.lParamLo, Msg.lParamHi);
         CurrentLine := New(PLine, Init(CommonPen));
         Drawing^.Insert(CurrentLine);
       end;
     end.
     procedure TStepWindow.WMMouseMove(var Msg: TMessage);
     begin
       if ButtonDown then
       begin
         LineTo(DragDC, Msg.lParamLo, Msg.lParamHi);
         CurrentLine^.AddPoint(Msg.LParamLo, Msg.LParamHi);
       end;
     end;

    Примечание: Уничтожать устаревшие CurrentLine не требуется, поскольку они записаны в наборе Drawing. Все объекты линий уничтожаются при уничтожении Drawing. WMLButtonUp модификации не требует. Вам не нужно уничтожать все объекты линий при очистке отображаемого окна, поэтому добавьте в CMFileNew вызов метода FreeAll:

     procedure TStepWindow.CMFileNew(var Msg: TMessage);
     begin
       Drawing^.FreeAll;
       InvalidateRect(HWindow, nil, True);
     end;

Вывод сохраненной графики


    Теперь, когда TStepWindow сохраняет свою текущую строку, вы должны научить его по команде (этой командой является Paint) рисовать ее. Давайте напишем для TStepWindow метод Paint, который повторяет действия WMLButtonDown, WMMouseMove и WMLButtonUp. Путем итерации по набору линий Paint воссоздает картинку аналогично тому, как это делаете вы. Метод Paint имеет следующий вид (см. файл STEP07.PAS):

     procedure TStepWindow.Paint(PaintDC: HDC; var PaintInfo:
                                 TPintStruct);
     procedure DrawLine(P: PLine); far;
     begin
       P^.Draw(PaintDC);
     end;
     begin
       Drawing^.ForEach(@DrawLine);
     end;

    Примечание: Итерация методов описывается в Главе 19 "Наборы".

    Метод Draw объекта линии для изображения каждой линии между точками также использует итератор ForEach:

     procedure TLine.Draw(ADC: HDC);
     var First: Boolean;
       procedure DrawLine(P: PLinePoint); far;
       begin
         if First then MoveTo(ADC, P^.X, P^.Y)
         else LineTo(ADC, P^.X, P^.Y);
         First := False;
       end;
     begin
       First := True;
       LinePen^.Select(ADC);
       Points^.ForEach(@DrawLine);
       LinePen^.Delete;
     end;

Шаг 8: Сохранение рисунка в файле


    Теперь, когда вы сохранили представление данных в виде рисунка как часть оконного объекта, можно легко записать эти данные в файл (фактически, в буферизованный поток DOS) и считать его обратно.

    Данный шаг посвящен добавлению полей объектов для записи состояния сохраняемой информации, модификации сохраняемой в файле информации и методам открытия. Используя предусмотренные в ObjectWindows потоковые объекты, вы убедитесь в удобстве их применения для сохранения данных в файле.

Отслеживание состояния


    Требуется отслеживать две характеристики рисунка. Изменение файла мы уже отслеживали (в шаге 1 было добавлено поле HasChanged), но теперь нужно знать, загружен ли файл в данный момент. Как и HasChanged, IsNewFile - это атрибут TStepWindow типа Boolean, поэтому его также следует сделать полем:

     TStepWindow = object(TWindow)
         ButtonDown, HasChanged, IsNewFile: Boolean;
           .
           .
           .
     end.

    Поле HasChanged принимает значение True, если текущий рисунок модифицирован. Модификация означает, что рисунок был изменен с момента последнего сохранения или не сохранялся вовсе. Вы уже устанавливаете поле HasChanged в True, когда пользователь начинает рисовать, и в False, когда окно очищается. Когда пользователь открывает новый файл или сохраняет существующий, HasChanged следует установить в False.

    IsNewFile указывает, что рисунок не взят из файла, поэтому сохранение рисунка потребует от пользователя задать имя файла. IsNeFile имеет значение True только при первоначальном запуске приложения и после выбора пользователем команды меню File|New (Файл|Новый). Это поле устанавливается в False, когда файл открывается или сохраняется. Фактически, FileSave использует IsNewFile, чтобы увидеть, можно ли сохранить файл немедленно, или пользователю требуется выбрать файл из файлового диалога.

    Приведем методы сохранения и загрузки файла. На данный момент они выполняют только сохранение и загрузку файлов. Сохранение файла сконцентрировано в одном новом методе, который называется WriteFile, а открытие файла выполняет метод ReadFile.

     procedure TStepWindow.CMFileNew(var Msg: TMessage);
     begin
       if CanClose then
       begin
         Drawing^.FreeAll;
         InvalidateRect(HWindow, nil, True);
         HasChanged := False;
         IsNewFile := True;
       end;
     end;
     procedure TStepWindow.CMFileOpen(var Msg: TMessage);
     begin
       if CanClose then
          if Application^.ExecDialog(New(PFileDialog, Init(@Self,
              PChar(sd_FileOpen), StrCopy(FileName, '*.PTS')))) =
              id_Ok then ReadFile;
     end;
     procedure TStepWindow.CMFileSave(var Msg: TMessage);
     begin
       if IsNewFile then CMFileSaveAs(Msg) else WriteFile;
     end;
     procedure TStepWindow.CMFileSaceAs(var Msg: TMessage);
     begin
       if IsNewFile then StrCopy(FileName, '');
       if Application^.ExecDialog(New(PFileDialog,
            Init(@Self, PChar(sd_FileSave), FileName))) =
            id_Ok then WriteFile;
     end;
     procedure TStepWindow.ReadFile;
     begin
       MessageBox(HWindow, @FileName, 'Загрузить файл:',
                  mb_Ok);
       HasChanged := False;
       IsNewFile := False;
     end;
     procedure TStepWindow.WriteFile;
     begin
       MessageBox(HWindow, @FileName, 'Сохранить файл:',
                  mb_Ok);
       HasChanged := False;
       IsNewFile := False;
     end;

    Примечание: Данный текст программы можно найти в файле STEP08A.PAS.

Сохранение и загрузка файлов


    Теперь, когда вы создали основную схему для построения и загрузки файлов, осталось только выполнить фактическую загрузку и сохранение в файле наборов точек. Для этого можно использовать потоковый механизм автоматического сохранения объекта. Сначала вы научитесь сохранять и загружать сами объекты точек и линий (как это сделать для наборов вы уже знаете). Затем методы WriteFile и FileOpen будут модифицированы для использования потоков.

    Примечание: Подробнее об использовании потоков с объектами рассказывается в Главе 20 "Потоки".

    Ниже приведен исходный код, показывающий как сохранять и загружать сами объекты TLine и TLinePoint:

     const
        RLinePoint: TStreamRec = (
           ObjType: 200;
           VmtLink: Ofs(TypeOf(TLinePoint)^);
           Load: @TLinePoint.Load;
           Store: @TLinePoint.Store);
        RLine: TStreamRec = (
           ObjType: 201;
           VmtLink: Ofs(TypeOf(TLine)^);
           Load: @TLine.Load;
           Store: @TLine.Store);
     constructor TLinePoint.Load(var S: TStream);
     begin
       S.Read(X, SizeOf(X));
       S.Read(Y, SizeOf(Y));
     end;
     procedure TLinePoint.Store(var S: TStream);
     begin
       S.Write(X, SizeOf(X));
       S.Write(Y, SizeOf(Y));
     end;
     constructor TLine.Load(var S: TStream);
     begin
       Points := PCollection(S.Get);
       LinePen := PPen(S.Get);
     end;
     procedure TLine.Store(var S: TStream);
     begin
       S.Put(Points);
       S.Put(LinePen);
     end;
     procedure StreamRegistration;
     begin
       RegisterType(RCollection);
     end;

    Для регистрации TCollection при запуске прикладной программы вы должны вызывать StreamRegistration (который находится в Steps). Вы можете поместить это вызов в метод TStepWindow.Init. Модуль DrawLine регистрирует в своем коде инициализации TLinePoint и TLine, поэтому линии и точки регистрируются простым включением DrawLine в оператор uses.

    Заключительным шагом изменения методов WriteFile и ReadFile будет фактическая запись в потоки и чтение из них (см. STEP08B.PAS):

     procedure TStepWindow.ReadFile;
     var
       TempColl: PCollection;
       TheFile: TDosStream;
     begin
       TheFile.Init(FileName, stOpen);
       TempColl: := PCollection(TheFile.Get);
       TheFile.Done;
       if TempColl <> nil then
       begin
         Dispose(Drawing, Done);
         Drawing := TempColl;
         InvalidateRect(HWindow, nil, True);
       end;
       HasChanged := False;
       IsNewFile := False;
     end;
     procedure TStepWindow.WriteFile;
     var
       TheFile: TDosStream;
     begin
       TheFile.Init(FileName, stCreate);
       TheFile.Put(Drawng);
       TheFile.Done;
       IsNewFile := False;
       HasChanged := False;
     end;

Шаг 9: Печать графического образа


    Печать из Windows может представлять собой сложную задачу, но ObjectWindows предоставляет простой механизм добавления средств печати в вашу прикладную программу.

    Добавление этих средств предусматривает следующие три шага:

Построение объекта принтера


    Любая программа ObjectWindows может получить доступ к принтеру с помощью объекта типа TPrinter. В этом случае основное окно вашего приложения должно построить объект принтера и сохранить его в объектном поле с именем Printer:

     constructor TStepWindow.Init(AParent: PWindowsObject;
                                  ATitle: PChar);
     begin
       inherited Init(AParent, ATitle);
        .
        .
        .
       Printer := New(PPrinter, Init);
     end;

    Примечание: Тип TPrinter определен в модуле OPrinter, поэтому не забудьте добавить OPrinter в свой оператор uses.

    Это все, что обычно приходится делать для инициализации объекта принтера. По умолчанию TPrinter использует назначенный по умолчанию принтер, заданный в файле WIN.INI. TPrinter предусматривает также механизм для выбора альтернативных принтеров.

Создание объекта распечатки


    ObjectWindows управляет выводимыми на печать данными точно также, как выводом на экран. То есть вместо записи непосредственно на устройство вывода (или даже непосредственно в Windows) вы направляете свой вывод на объект, который знает, как представлять информацию на устройстве вывода. Для вывода на печать используйте объекты, полученные из абстрактного типа TPrintout и метода с именем PrintPage.

    Существует два основных случая, с которыми вы будете иметь дело при генерации из приложения Windows данных для печати: печать документов и печать содержимого окна. Печать содержимого окна проще, поскольку окна уже "знают" о своем представлении. Как оказывается на самом деле, это просто особый случай печати документа.

    Примечание: О печати документов рассказывается в Главе 15.

Запись в контекст устройства


    До настоящего момента вы всегда записывали текст и графику в контекст дисплея (что является способом представления в Windows пользовательской области окон). Контекст дисплея - это просто специализированная версия контекста устройства - механизма, через который приложения Windows работают с различными устройствами, такими как экраны, принтеры или коммуникационные устройства.

    Запись в контекст одного устройства мало отличается от записи в контекст другого, поэтому, например, достаточно просто сообщить, например, оконному объекту, что для записи на принтер нужно использовать механизм Paint.

Создание распечатки окна


    TWindowPrint - это специальный потомок TPrintout, используемый для печати содержимого окна. Печать содержимого окна не представляет сложности по двум причинам: вы имеете дело только с одной страницей, и объект окна уже знает, как отображать свой образ.

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

    Таким образом, печать также проста, как ориентация метода Paint объекта окна на вывод вместо окна в контекст устройства, подходящий для принтера:

     procedure TWindowPrint.PrintPage(DC: HDC; Page: Word;
                     Size: TPoint; var Rect: TRect; Flags: Word);
     var PS: TPaintStruct;
     begin
       Window^.Paint(DC, PS);
     end;

    Поскольку переданный PrintPage параметр DC уже указывает на подходящий для принтера контекст устройства, в простейшем случае PrintPage должен только сообщить объекту окна на вывод его содержимого в этот контекст устройства. Теперь ваш объект распечатки также знает о своем представлении.

Вывод распечатки


    Наличие объекта распечатки, которому известно о своем представлении, это все, что нужно для передачи распечатки на принтер. Программа Steps делает это в ответ на команду cm_FilePrint, генерируемую командой Print меню File:

     procedure TStepWindow.CMFilePrint(var Msg: TMessage);
     var P: PPrintout;
     begin
       if IsNewFile then StrCopy(FileName, 'Untitled');
       P := New(PWindowPrint, Init(FileName, @Self));
       Printer^.Print(@Self, P);
       Dispose(P, Done);
     end;

    CMFilePrint очень просто строит объект распечатки, озаглавленный заданным именем (имя файла точек или 'Untitled') и заполняет его своим содержимым (так как это единственное окно в приложении).

    При наличии объекта распечатки CMFilePrint сообщает объекту принтера, что его нужно напечатать, добавив какие-либо сообщения об ошибках или диалоговые окна (отсюда параметр @Self). Когда печать закончится, CMFilePrint уничтожает объект распечатки.

Выбор другого принтера


    Одним из средств TPrinter является возможность изменения установки принтера. TPrinter определяет метод Setup, который выводит диалоговое окно установки принтера, позволяющее пользователю выбрать принтер из числа установленных в Windows и обеспечивает доступ к диалоговому окну конфигурации устройства.

    Чтобы вывести диалоговое окно установки принтера, ваша прикладная программа вызывает метод Setup объекта принтера. Steps делает это в ответ на команду cm_FileSetup (см. STEP09.PAS):

     procedure TStepWindow.CMFileSetup(var Msg: TMessage);
     begin
       Printer^.Setup(@Self);
     end;

    Диалоговое окно установки принтера является экземпляром типа TPrinterSetupDlg (см. Рис. 5.1).


Рис. 5.1 Диалоговое окно установки принтера.

    В комбинированном блоке диалогового окна выводятся принтеры, заданные в WIN.INI. Это дает пользователю возможность доступа ко всем установленным принтерам.