Создание простого конструктора бланков на C#

Автор: Мухаммед Эльшейми (Mohammad Elsheimy)

Конструктор бланков предоставляет пользователям гибкие возможности создания бланков, отчетов, счетов, счетов-фактур и других пользовательских документов.

Введение

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

В этой статье все отчеты, счета, счета-фактуры, рецепты и т. д. мы будем называть просто бланками. Поэтому нам понадобится дать определение бланка.

Бланк — это просто пустая страница (т. е. шаблон), содержащая элементы бланка. Например, шаблон резюме — это бланк, содержащий много элементов (имя, дата рождения, адрес, и т. д.). Приходный кассовый ордер — это бланк, содержащий несколько элементов (дата, сумма, на чей счет отнесено, кем получено и т. д.).

Мы подробно разберем это средство. Начнем с очень простого системного анализа и схемы приложения. Затем перейдем к кодированию.

На самом деле я не архитектор и, думаю, никогда им не буду. Поэтому НЕ ругайте меня за этот плохой анализ. Он — просто иллюстрация, призванная дать цельное понимание того, для чего предназначено это приложение.

Проблема

Пользователь привязан к макетам бланков, разработанных для пользователя дизайнером. Но ему нужна возможность создавать собственные бланки и/или изменять бланки, определенные в приложении.

Требования

Требования пользователя

Ниже перечислены типовые пользовательские требования:

  • Приложение должно позволять пользователю создавать, изменять и удалять бланки, а также группировать бланки в созданные пользователем категории.
  • Для простоты будет предусмотрен только один уровень этих категорий, вложенность категорий не разрешается.
  • Приложение должно быть долговременным. Данные должны сохраняться в базе данных и загружаться, когда они понадобятся пользователю.
  • Бланк должен загружаться с правильными данными и печататься, как только пользователь это потребует.
  • Как и в Visual Studio, у пользователя должна быть панель инструментов, содержащая все типы элементов бланков. Пользователь может вставить элемент бланка в свой бланк, рисуя его на экране, перетаскивая его с панели инструментов или дважды щелкая его значок на панели инструментов.
  • У пользователя должна быть линейка, позволяющая ему определять размеры элементов бланка. Кроме того, для него должна быть предусмотрена сетка, видимая только в режиме конструктора.
  • У пользователя должна быть возможность задавать атрибуты бланка, такие как параметры страницы, параметры сетки, цвет фона и т. д.
  • Не все атрибуты бланка печатаются — некоторые предназначены только для режима конструктора (например цвет фона и сетка.)
  • У каждого типа элементов бланка должны быть свои атрибуты, и пользователю должно быть также позволено их изменять.
  • Пользователю должно быть разрешено перетаскивать элементы бланка по экрану и размещать их на бланке.
  • У пользователя должны быть возможность обрезать, копировать, вставлять и удалять элементы бланка.
  • Конечно же, пользователю также должно быть разрешено печатать бланк и выполнять его предварительный просмотр.
  • Приложение должно быть достаточно общим, чтобы использоваться в любой системе и для любого применения.
  • У других компонентов системы должна быть возможность взаимодействовать с бланком.

Функциональные требования

Общими функциональными требованиями (требованиями к разработчику) являются следующие:

  • Абстракция. При разработке должна использоваться абстракция компонента/класса. Система должна состоять из нескольких компонентов, каждый из которых объединяет вместе связанные функции (например объекты интерфейса, бизнес-объекты и управление данными).
  • Расширяемость. Система должна быть расширяемой. Должна быть разработана хорошая иерархия классов, учитывающая возможность создания производных классов (наследования). Кроме того, каждый элемент бланка должен быть представлен классом в этой иерархии.
  • Технически элементы бланка можно было бы назвать фигурами. Так как они — просто чертежи на эскизе (или бланке.)
  • Чтобы позволить пользователю работать с элементами бланка (фигурами), а приложению — заполнить бланк данными, элементы бланка не следует рисовать прямо на странице бланка. Вместо этого, каждый элемент должен представляться классом со своей собственной процедурой рисования.
  • Бланк должен быть контейнером для фигур. Все фигуры являются дочерними объектами бланка.
  • Бланк и линейки должны быть элементами управления Windows, чтобы они могли размещаться в форме Windows Form.
  • Элементы списка также должны быть элементами управления Windows, чтобы они могли размещаться в бланке.
  • Элементы бланка должны быть элементами управления, рисуемыми владельцем, и они не должны быть производными от существующих элементов управления Windows, чтобы их можно было легко встраивать в бланк.
  • Ради простоты для бланка и фигур должна быть предусмотрена возможность сериализации в XML и сохранения в базу данных.
  • База данных должна быть базой данных SQL Server, чтобы обеспечить более быстрое взаимодействие и более простую работу с XML-данными.
  • База данных должна содержать три таблицы для трех основных компонентов системы: категории, бланки и фигуры.
  • У каждого элемента бланка в данном бланке должно быть уникальное имя (или тег), чтобы позволить другим компонентам системы взаимодействовать с этим элементом (например заполнять его правильными данными.)
  • У разработчика должна быть возможность независимо управлять качеством процесса рисования для каждого элемента управления или элемента бланка.

Решение

После разработки требований к проекту и анализа сложного дизайна системы мы получаем хороший план для нашего проекта, который будет называться Geming SISC (Sheet Infrastructure Components, компоненты инфраструктуры списка).

Этот проект будет создан с помощью C# и .NET 2.0 (или, конечно же, более поздних версий.)

Снимки экранов

Ниже приведены конечные снимки экранов приложения. На рис. 1 показан бланк приходного кассового ордера с надписями, полями и линиями. На рис. 2 показан простой шаблон резюме, разработанный с использованием только нескольких надписей и картинки. В левой области приложения перечислены текущие бланки, сгруппированные по категориям. 

Как известно, другие компоненты системы моли заполнить эти поля правильными данными.

Разработка компонентов

На рис. 3 показаны три компонента нашей системы. 

Тремя компонентами нашей системы, Geming SISC, являются:

  • Geming.Sisc.Infrastructure.
    Содержит элемент управления для бланка и элементы управления для фигур (надпись, текстовое поле и т. д.). Элементы управления для бланка, линеек и элементов бланка являются производными от класса System.Windows.Forms.Control.
  • Geming.Sisc.Data.
    Бизнес-объекты, которые будут передавать данные между объектами диспетчера базы данных. Этот компонент ссылается на компонент Geming.Sisc.Infrastructure.
  • Geming.Sisc.Driver.
    Интерфейс приложения, который будет использоваться для конструирования бланков. Он ссылается на другие два компонента.

Схемы классов

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

Ниже показана иерархия классов для элементов управления бланка и линеек. 

Как можно видеть, базовым является класс System.Windows.Forms.Control. Это позволяет вставлять элементы управления бланка и линеек в форму Windows или элемент управления Windows.

На следующем рисунке, рис. 5, показана иерархия классов элементов бланка (формально называемых фигурами.)

Как можно видеть, базовым классом для всех фигур является абстрактный класс Geming.Sisc.Infrastructure.ShapeBase, наследуемый от класса System.Windows.Forms.Control. Все остальные фигуры являются производными от ShapeBase.

Были созданы две редактируемые фигуры: TextBoxShape, выглядящая как текстовое поле Windows, и LabelShape, выглядящая как обычная надпись Windows. Оба класса являются производными от абстрактного класса EditableShapeBase.

Другими фигурами являются: BoxShape (прямоугольник), LineShape (линия), ImageShape (картинка) и CheckBoxShape (флажок).

Все классы являются сериализуемыми (реализуют интерфейс System.Runtime.Serialization.ISerializable), поэтому их можно легко преобразовать в XML и поместить в базу данных.

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

Более подробные схемы классов, показывающие, например, члены классов, см., открыв код приложения.

Схема базы данных

В своей простоте база данных определяется как следующая схема: 

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

Столбец Shape.Value является столбцом типа xml, чтобы упростить обработку XML-данных в будущем.

Характеристики

Вот некоторые характеристики (т.е. атрибуты) бланка (некоторые из них представлены свойствами):

  • Цвет фона.
    У пользователя должна быть возможность изменить цвет фона. Обратите внимание, что цвет фона не печатается.
  • Непечатаемая сетка.
    Помогает пользователю размещать элементы. У пользователя должна быть возможность включать и отключать ее отображение. Кроме того, пользователь может изменить цвет сетки.
  • Поле.
    Приложение должно задавать поля страницы в зависимости от настроек печати.
  • Заголовок.
    У каждого бланка есть заголовок, описание и категория.

Кроме того, ниже перечислены некоторые характеристики фигуры (элемента бланка):

  • Выделена.
    Выделена ли в данный момент фигура или нет. Пользователь может выделить фигуру мышью. Вокруг выделенной фигуры рисуется рамка выделения.
  • Непечатаемая "ручка" для изменения размера.
    У каждой фигуры должна быть "ручка", позволяющая пользователю изменить размер фигуры.
  • Клонирование.
    Должна быть предусмотрена возможность клонирования, то есть копирование для последующей вставки.

Предыстория

Вот несколько приемов, используемых в этой системе.

  • Рисование

В некоторых случаях мы используем пользовательское рисование. Следовательно, мы воспользуемся помощью классов System.Drawing, конкретно класса System.Drawing.Graphics.

В некоторых фигурах, подражающих существующим элементам управления Windows (таким как текстовое поле и флажок), классы из пространства имен System.Windows.Forms.VisualStyles помогут нам отразить в наших элементах управления визуальные стили Windows. Кроме того, класс System.Windows.Forms.ControlPaint используется для рисования границ и прямоугольников выделения.

  • Качество рисования

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

  • Сериализация

Все сериализованные объекты должны быть помечены атрибутом System.SerializableAttribute. Настроить процесс сериализации нам поможет пространство имен System.Runtime.Serialization.

Основным интерфейсом, который позволил бы нам настроить процесс сериализации, является интерфейс System.Runtime.Serialization.ISerializable (реализованный во всех сериализуемых классах.) Обратите внимание, что нам следует добавить конструктор десериализации, чтобы получить правильную десериализацию.

  • База данных

Базой данных является клиентская база данных SQL Server, в которой все операции выполняются с помощью хранимых процедур.

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

  • Поддержка режима конструктора

Для расширения поддержки режима конструктора мы создали настраиваемую службу конструктора для класса Sheet, наследуемого от класса System.Windows.Forms.Design.ParentControlDesigner, чтобы позволить использовать вложенные элементы управления в объекте Sheet в режиме конструктора.

Выделенные фрагменты кода

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

  • Процедуры рисования

Ниже приведен код для процедуры OnPaint(), переопределяемой только классом ShapeBase.

Пример кода 1. Пример кода метода ShapeBase.OnPaint()

protected override void OnPaint(PaintEventArgs e)
{
    GraphicsManager.SetQuality(e.Graphics, PaintingQuality);

    ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, this.ForeColor, ButtonBorderStyle.Dotted);

    PaintShape(e.Graphics);

    if (Selected)
        DrawSelectionFrame(e.Graphics);

    ControlPaint.DrawSizeGrip(e.Graphics, this.BackColor, GetResizeGripRect());
}

Эта функция сначала вызывает функцию GraphicsManager.SetQuality(), определяющую атрибуты качества объекта Graphics. Мы скоро вернемся к этой функции.

Затем функция рисует границу элемента управления, используя класс System.Windows.Forms.ControlPaint.

Наступает интересный момент. Функция вызывает виртуальную функцию PaintShape(), которую производный класс переопределяет, предоставляя собственные процедуры рисования.

Например, у класса TextBoxShape есть своя переопределенная функция PaintShape() override.

Пример кода 2. Пример кода метода TextBoxShape.PaintShape()

public override void PaintShape(Graphics dc)
{
    base.PaintShape(dc);


    using (Brush b = new SolidBrush(this.BackColor))
        dc.FillRectangle(b, this.ClientRectangle);

    using (Pen p = new Pen(SystemColors.ActiveCaption, 1))
    {
        p.Alignment = System.Drawing.Drawing2D.PenAlignment.Inset;
        Rectangle r = this.ClientRectangle;
        dc.DrawRectangle(p, r);
    }

    Rectangle rect = this.ClientRectangle;
    rect.Offset(2, 2);
    rect.Width -= 4; rect.Height -= 4;

    StringFormat format = new StringFormat();
    format.LineAlignment = this.Alignment;
    if (this.RightToLeft == RightToLeft.Yes)
        format.FormatFlags = StringFormatFlags.DirectionRightToLeft;

    using (Brush b = new SolidBrush(this.ForeColor))
        dc.DrawString(this.Text, this.Font, b, rect, format);
}

Затем функция OnPaint() рисует рамку выделения, если фигура в данный момент выбрана.

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

  • Качество рисования

В нашей библиотеке Geming.Sisc.Infrastructure мы создали вспомогательный класс GraphicsManager, содержащий только одну функцию, SetQuality().

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

Пример кода 3. Пример кода метода GraphicsManager.SetQuality()

public static void SetQuality(System.Drawing.Graphics g, PaintingQuality quality)
{
    if (g == null)
        throw new ArgumentNullException("g");
    if (quality == PaintingQuality.High)
    {
        g.CompositingQuality = CompositingQuality.AssumeLinear;
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;
        g.PixelOffsetMode = PixelOffsetMode.Half;
        g.SmoothingMode = SmoothingMode.AntiAlias;
        g.TextRenderingHint =
            System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
    }
    else if (quality == PaintingQuality.Medium)
    {
        g.CompositingQuality = CompositingQuality.HighQuality;
        g.InterpolationMode = InterpolationMode.Bilinear;
        g.PixelOffsetMode = PixelOffsetMode.HighQuality;
        g.SmoothingMode = SmoothingMode.HighQuality;
        g.TextRenderingHint =
            System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
    }
    else
    {
        g.CompositingQuality = CompositingQuality.Default;
        g.InterpolationMode = InterpolationMode.Default;
        g.PixelOffsetMode = PixelOffsetMode.Default;
        g.SmoothingMode = SmoothingMode.Default;
        g.TextRenderingHint =
            System.Drawing.Text.TextRenderingHint.SystemDefault;
    }
}