« Poland Will Appear AgainUPS Follies »

Delphi .NET Fun

11/15/06

  12:38:38 am, by Nimble   , 2337 words  
Categories: Thoughts, Programming

Delphi .NET Fun

I'm making a card game - a physical one, not a computer one, so I decided to give myself both a learning experience and a tool to help print out the cards by pulling down Turbo Delphi for .NET at home and try to program the entire thing with the .NET-only WinForms instead of the VCL.

Despite some setbacks, the tool took me just the greater part of a day, and I got to learn a few odd things while I was at it.

The experience also reinforced my view that it really is the library far more than the language involved that makes most of the learning curve.

One thing I wanted to take some advantage of, since I was going for WinForms and basically needed to put in a lot of settings (number of cards on a page, names of clip art, etc.), was the PropertyGrid.

You essentially simply assign its SelectedObject property to an object you want to edit the properties on.

In Delphi for .NET, putting a property in the published section adds the [Browsable(True)] attribute to the property, which will make it appear in the property grid.

There are other attributes you can add, some of the more useful being [Description('description')], [Category('category')] and [Editor(typeof(editor),typeof(UITypeEditor)] (more on the latter one later.

The Category attribute sorts your property into a category in the property grid. You can choose any text, as far as I've been able to see. The Description attribute decides what will display at the bottom of the property grid.

e.g.

  CardPreferences = class
  private

    _CardXFirst : Double;
    _CardXOffset : Double;

    _CardXWidth : Double;
  published

    [Description('Distance to left side of first card '+
     'in inches'), Category('Card Layout')]

    property CardXFirst: Double
      read _CardXFirst write _CardXFirst;

    [Description('Offset to left of next card '+
     'in inches'), Category('Card Layout')]

    property CardXOffset: Double
      read _CardXOffset write _CardXOffset;

    [Description('Width of card in inches'),
     Category('Card Layout')]

    property CardXWidth: Double
      read _CardXWidth write _CardXWidth;

  end;

Sometimes, plain numbers and text alone in the property grid just aren't adequate. For example, if you want to have a property treated as a file name, typing in the whole file name by hand isn't a lot of fun.

There is actually a FileNameEditor you can use, but this alone does not give you the control you may want. You can actually override the FileNameEditor fairly simply, and use it on your own properties:


unit Ritchie.XmlFileProperty;


interface


uses
  System.Windows.Forms.Design, System.Windows.Forms;


type

  XmlFileNameEditor = class(FileNameEditor)
  strict protected

    procedure InitializeDialog(openFileDialog: OpenFileDialog); override;
  end;


implementation


{ XmlFileNameEditor }


procedure XmlFileNameEditor.InitializeDialog(openFileDialog: OpenFileDialog);

begin
  inherited;

  openFileDialog.Filter := 'XML files (*.xml)|*.xml|'+
    'All files (*.*)|(*.*)';

end;


end.

You then add this editor in with the Editor attribute, like so:


    [Description('Location of special cards file'),
     Editor(typeof(XmlFileNameEditor),

            typeof(UITypeEditor)),
     Category('Special Cards')]

    property SpecialCards: System.String
      read _SpecialCards write _SpecialCards;

Now, when I click on my "SpecialCards" property in the property grid, it has a [...] button next to it, which I can click to find an appropriate file (an .xml file, by default).

Now, once I had the property grid working more or less the way I wanted, I started needing a way to save the properties in and out. For this exercise, I chose to use the XmlSerializer, which would save and load my properties out in XML.

I added the [XmlRootAttribute] attribute above my class name:

type
  [XmlRootAttribute]

  CardPreferences = class

Making the Save code (here, run from a Save button) is relatively easy. The _CardPreferencesObject is the object I want to save, and CardPreferences is the class to which it belongs. It's easiest to create a Writer and then Serialize the XML out to it, as shown below.

procedure TWinForm.SaveButton_Click(sender: System.Object;
  e: System.EventArgs);

var
  Writer : StreamWriter;

begin
  if SavePreferencesDialog.ShowDialog=

     System.Windows.Forms.DialogResult.OK then
  begin

    Writer :=
      StreamWriter.Create(SavePreferencesDialog.FileName,

      False,System.Text.Encoding.UTF8);
    try

      XmlSerializer.Create(typeof(CardPreferences)).
        Serialize(Writer,_CardPreferencesObject);

    finally
      Writer.Close;

    end;
  end;

end;

Loading is almost the same, though one thing you will have to watch out for is that Deserialize creates an entirely new object. Even if you replace the current object with it, you will have to give the property grid the new object as a SelectedObject:

procedure TWinForm.LoadButton_Click(sender: System.Object;
  e: System.EventArgs);

var
  Reader : StreamReader;

begin
  if OpenPreferencesDialog.ShowDialog=

     System.Windows.Forms.DialogResult.OK then
  begin

    Reader := StreamReader.Create(
      OpenPreferencesDialog.FileName,True);

    try
      _CardPreferencesObject := XmlSerializer.Create(

        typeof(CardPreferences)).Deserialize(Reader)
          as CardPreferences;

      MyPropertyGrid.SelectedObject := _CardPreferencesObject;
    finally

      Reader.Close;
    end;

  end;
end;

Everything was going great guns until I decided that I wanted to be able to choose a font.

Well, it turns out that Font objects do not work properly with XmlSerialization. The reason being that you cannot create a Font without any arguments. No Font.Create(); (new Font(); in C#)

This is a bit of a pickle, really, since otherwise, the property grid lets you edit the font very nicely. You just can't load or save it to XML this way.

From James Johnson's .NET XML Serialization article, I cribbed a workaround. Namely, you make a new, brain-dead class with just Font's properties, then use that with serialization, with some tweaking.

First, the new XmlFont class:

unit Ritchie.XmlFont;


interface


uses
  System.Drawing;


type

  XmlFont = class
  private

    _Family : System.String;
    _Size : Double;

    _Style : FontStyle;
    _UnitType : GraphicsUnit;

  public
    constructor Create; overload;

    constructor Create(const AFont: Font); overload;
    function ToFont: Font;

  published
    property Family: System.String

      read _Family write _Family;
    property Size: Double read _Size write _Size;

    property Style: FontStyle read _Style write _Style;
    property UnitType: GraphicsUnit

      read _UnitType write _UnitType;
  end;


implementation


{ XmlFont }


constructor XmlFont.Create;

begin
  inherited Create;

end;


constructor XmlFont.Create(const AFont: Font);
begin

  inherited Create;
  _Family := AFont.FontFamily.Name;

  _UnitType := AFont.&Unit;
  _Size := AFont.Size;

  _Style := AFont.Style;
end;


function XmlFont.ToFont: Font;

begin
  Result := Font.Create(_Family,_Size,_Style,_UnitType);

end;


end.

The overloaded constructor that takes a Font, and the ToFont function will come in very handy here.

There are a few steps involved once you've done this.

First, add properties of the new type XmlFont to your class that is being serialized. Give them the attribute [XmlElement('name')], where the name will be how the property appears in the XML file:

  public
    function get_SpecialCardDescriptionXmlFont: XmlFont;

    function get_SpecialCardSpecialXmlFont: XmlFont;
    function get_SpecialCardTitleXmlFont: XmlFont;

    procedure set_SpecialCardDescriptionXmlFont
      (const Value: XmlFont);

    procedure set_SpecialCardSpecialXmlFont
      (const Value: XmlFont);

    procedure set_SpecialCardTitleXmlFont(const Value: XmlFont);
    [XmlElement('SpecialCardTitleFont')]

    property SpecialCardTitleXmlFont: XmlFont
      read get_SpecialCardTitleXmlFont

      write set_SpecialCardTitleXmlFont;
    [XmlElement('SpecialCardDescriptionFont')]

    property SpecialCardDescriptionXmlFont: XmlFont
      read get_SpecialCardDescriptionXmlFont

      write set_SpecialCardDescriptionXmlFont;
    [XmlElement('SpecialCardSpecialFont')]

    property SpecialCardSpecialXmlFont: XmlFont
      read get_SpecialCardSpecialXmlFont

      write set_SpecialCardSpecialXmlFont;

Next, add the [XmlIgnore] attribute to the normal Font properties, e.g.:

    [Description('Font for title on special cards'),
     Category('Special Cards'),

     XmlIgnore]
    property SpecialCardTitleFont: System.Drawing.Font

      read _SpecialCardTitleFont
      write _SpecialCardTitleFont;


Then, fill in those getters and setters for the XmlFont properties to map to and from the real Font properties, e.g.:

function CardPreferences.get_SpecialCardTitleXmlFont:
  XmlFont;

begin
  Result := XmlFont.Create(_SpecialCardTitleFont);

end;


procedure CardPreferences.set_SpecialCardTitleXmlFont(
  const Value: XmlFont);

begin
  _SpecialCardTitleFont := Value.ToFont;

end;

It works like a charm, but man oh man, finding out these little tricks in Microsoft's reflection and serialization systems can be taxing.

So things are coming together quite well, now it was time for me to tackle the printing.

The general structure of the printing system is actually quite nice. The PrintDocument class gives you events for start of print (BeginPrint), end of print (EndPrint), a means to print the page, and an event that lets you change the settings on each page (QueryPageSettings, a welcome relief from having to suss out the Windows API way to do those changes mid-print job).

The System.Drawing.Printing.PrintPageEventArgs object given to you in the PrintPage event is quite full of information, and includes a Graphics object, which you use to draw things on the printer page.

Assigning a PrintPreviewDialog's Document to your PrintDocument essentially takes care of business for print previewing. All you have to do is PrintPreviewDialog.ShowDialog; at that point, and everything spins up and print previews very nicely.

For actual printing, you seem to need to call PrintDocument.Print, but calling a PrintDialog first - assigning it a PrintDocument beforehand, which you can do in code or in the IDE - helps set things up:

procedure TWinForm.PrintButton_Click(sender: System.Object;
  e: System.EventArgs);

begin
  InitializeDeck;

  if PrintDialog.ShowDialog=
     System.Windows.Forms.DialogResult.OK then

    PrintDialog.Document.Print;
end;

Pretty nice, all things told.

That said, there are snags in the system. If you want definitive proof, try to do a print onto something where the coordinates really, really matter, like on Avery labels or cards.

First, there are little oddities like everything in the printing graphics system being in 1/100ths of an inch. I can deal with that.

However, there are margin issues, and it turns out that things have not improved much from 1.0 to 1.1, and I saw no indications of a fix in 2.0 (though I have yet to confirm that).

Essentially, even though PageBounds returns you {0, 0, 850, 1100} for 8 1/2 x 11 " paper, it's actually offsetting that by an amount equal to the printer's hard margins where it will not print. PageBounds does not indicate this to you at all.

There is a setting in PrintDocument in .NET 1.1 called OriginAtMargins. Some people have found this helps them get more accurate printing. That said, the Print Preview appears to ignore this property completely, meaning you have to do adjustments yourself to your print code. Also, I ended up with slightly odd printing on printout - if I went outside the MarginBounds (which I had to for this Avery card set), I got some very strange printing on the printout but not the preview, such as one of my cards popping out from the corner.

You essentially just have to break down and grab the physical offsets yourself, meaning interfacing with good old Windows libraries directly (*sigh*):

[System.Runtime.InteropServices.DllImport
  ('gdi32.dll', SetLastError=true)]

function GetDeviceCaps(hdc: IntPtr; nIndex: Integer):
  Integer; external;

...and calculate the hard offset you need thusly:

function DeckPrintManager.CalculateRawPrinterOffsets(
  AArgs: System.Drawing.Printing.PrintPageEventArgs):

    PointF;
const

  PHYSICALOFFSETX = 112;
  PHYSICALOFFSETY = 113;

var
  hDC : IntPtr;

  OffsetX : Double;
  OffsetY : Double;

begin
  hDC := AArgs.Graphics.GetHdc();

  try
    OffsetX := GetDeviceCaps(hDC,112);

    OffsetY := GetDeviceCaps(hDC,113);
  finally

    AArgs.Graphics.ReleaseHdc(hDC);
  end;

  Result := PointF.Create
    ((OffsetX*100/AArgs.Graphics.DpiX),

     (OffsetY*100/AArgs.Graphics.DpiY));
end;

Subtract the resulting coordinates from your printing coordinates, and you will find a much more satisfactory result.

I finally got what I was looking for. I hope this helps another poor soul stumbling over the same issues.

Until next time, farewell.

No feedback yet