This seems like my first post in the New Year (btw, my blog is almost a year old now!). Well, let’s start the year with colors then.
Nearly two years ago I’ve written two functions to write colorful messages to Windows console based on a string pattern – in other works, without manual calls to SetConsoleTextAttribute. Since then my experience has advanced particularly well so I decided to rewrite those two functions that were somewhat limited anyway.
This is how the ColorConsole unit was made. It works in Delphi 7 (perhaps in more recent versions too) and uses my D7X – Delphi 7 eXtension library library.
In most simplest case ColorConsole accepts a string which contains special commands put inside curly braces ({ and }). Each command is represented by a separate class; there are 7 standard classes (in this order of priority as defined in default CCParsing.Parts):
{ and } symbols can be escaped (that is, output as is) by prefixing them with extra { so that { becomes {{ and } becomes {}. There are no other special symbols in ColorConsole. See also CCQuote.
The brace constructs (of one or different types) can be nested into each other without limitations – for example:
ColorConsole is released in public domain – feel free to use it as you like.
You can download the unit and the demo program here. ColorConsole requires D7X – Delphi 7 eXtension library library.
To use ColorConsole you need Delphi 7 and my D7X – Delphi 7 eXtension library library. When you’re set up outputting a format string is trivial:
WriteColored function parses and outputs the input string, using TCCParsedStr class that is the core of the ColorConsole. Other API functions are simply wrapper for its methods that use default parsing (CCParsing) and output (CCWriting) options.
If specified, WriteColored also caches the parsed string and if the function is called again with the same string and parsing options reuses previously generated object.
To have more control over the process TCCParsedStr class can be used directly.
pascal// uses default CCParsing and CCWriting options: procedure WriteColored(const Str: Widestring; Cache: Boolean = True); overload; // parses and outputs Str, optionally caching it avoiding consequent reparsing: procedure WriteColored(const Str: Widestring; const ParsingOpt: TCCParsingOptions; const WritingOpt: TCCWritingOptions; Cache: Boolean = True); overload; // replaces '{' and '}' with '{{' and '{}' respectively: function CCQuote(const Str: WideString; const Opt: TCCParsingOptions): WideString; // returns a parsed string; doesn't use caching: function ParseCC(const Str: Widestring; const Opt: TCCParsingOptions): TCCParsedStr; // outputs a previously parsed string; its object is not freed: procedure OutputCC(const ParsedStr: TCCParsedStr; const Opt: TCCWritingOptions); // built-in default writer for TCCWritingOptions.Writer: function DefaultCCWriter: TCCWriter; // default set of vars (NL and TAB) for TCCWritingOptions.Variables: function DefaultCCVars: TCCVarList;
pascal// outputs a string to the console or other devide (Context.Options.Handle); // note that Writer isn't called if Str is empty: TCCWriter = procedure (Str: WideString; var Context {TCCWriting}) of object; // is called by writing process after associated TCCWriter was invoked: TCCPostWriter = procedure (var Context: TCCWriting; Start: TCoord) of object; // used by TCCVarList to call back to get an unlisted variable value: TCCOnUndefinedVar = function (Vars: TCCVarList; Name: WideString): WideString of object;
pascal{ Used when parsing a format string: } TCCParsingOptions = record Custom: Pointer; // can be called by the caller to store arbitrary data Opener, Closer: WideChar; // { and } Parts: array of TCCPartClass; { Settings for standard part classes (in Parts array) } OpenerDelimiter: WideChar; // space, e.g. in expression "{ri " AlignCenter: WideChar; // < AlignRight: WideChar; // > BGSeparator: WideChar; // @ Repeater: WideChar; // x; if #0 - needs none (e.g. "{55}") AbsFill: WideChar; // #0 HexChar: WideChar; // # (e.g. "{#2592}" NextHexChar: WideChar; // # (e.g. "{#AD#50C}" Variable: WideChar; // #0 (e.g. "{var_name}") end; { Used when outputting a parsed string: } TCCWritingOptions = record Custom: Pointer; // can be called by the caller to store arbitrary data Handle: THandle; // if this is True and Handle = StdOut and Writer is the default CC writer then // SetConsoleOutputCP(CP_UTF8) is called; also, it this is True all input is converted to // UTF-8 when writing to Handle; if this is False input is converted into current codepage. UTF8: Boolean; Writer: TCCWriter; VarGetter: TCCVarGetter; // are not freed automatically; by default contains "NL" and "TAB". end;
Contexts used when parsing or outputting a string:
pascal{ Parsing-time context: } TCCParsing = record Str: WideString; Pos: Integer; Options: TCCParsingOptions; end; { Output-time context: } TCCWriting = record Original: WideString; Coord: TCoord; LastPartIndex: Integer; PostWriters: TCCPostWriters; Options: TCCWritingOptions; end;
All ColorCOnsole exceptions are derived from standard Exception class. Base exception is EColorConsole%% from which others are inherited:
They have no special fields other than those of Exception.
This section briefly describes how ColorConsole works.
The concept revoles around the idea of string parts – each part have standard operations (writing, some have parsing) and properties (repetitions on write-time). All parts can be logically divided into plain text, repeating, outputting and grouping (collections) parts.
These are standard part classes, all deriving from TCCPart:
Everything starts with TCCParsedStr class. To parse a string it creates a root part group (TCCRootParts) which recursively does the actual parsing.
Parsing basically identifies beginnings of nested parts (starting with {), quoted { and } (when { appears as last character in the input or {/} are prefixed with a {) and plain text.
Nested parts are handled simply by iterating over TCCParsingOptions.Parts (which is an
pascalarray of TCCPartClass
) and calling each class’ TryParsing method – if it returns an object then a part is identified and is added to the collection, if all classes have returned NIL then the input contains a syntax error and parsing stops with a ECCParsing exception.
Since a preceding handler will override its followers the order of classes in Parts is important. For example, if you put TCCVarPart before anything else and at the same time set TCCParsingOptions.Variable to
pascal#0
(disabling any prefix that would appear after initial {) then no handlers will work because any occurrence of { … } construct will be treated as a variable reference.
When a string needs to be output TCCParsedStr (which contains parsed string representation) calls its root which prepares the writing context (TCCWriting) and starts recursively calling Write method of its children.
Also, if writing options have UTF8 set to
pascalTrue
and the output equals to stdout SetConsoleOutputCP is called on it; if it fails EOSError (via RaiseLastOSError) is raised.
When writing a list of post-writers is available. A post-writer is a callback that is called after an output has been writtern (by calling associated TCCWriter). This list is used by TCCColorPart to overlay colors in order they appear in the source string instead of innermost-first.
Consider the following example and the format string {r red {@b blue}}:
pascalprocedure TCCColorPart.Write(var Context: TCCWriting); begin inherited; // calls TCCParts.Write that recursively outputs its children. SetColors; // applies colors to the text just output. end;
What happens here? Since writing (as well as parsing) is done in recursive way the first color text to be output is «red». It contains a child, «blue», that is output by TCCParts.Write called via inherited. The «blue» text returns from inherited first and sets its colors; then the «red» text returns and sets its colors too. Now there’s just one color on the screen – red.
Post-writers list reverses this order and calls handlers in order they were added – as if they were placed before inherited. So the method now looks like this:
pascalprocedure TCCColorPart.Write(var Context: TCCWriting); begin Context.PostWriters.Add(Self.PostWrite); inherited; Context.PostWriters.Delete(Self.PostWrite); end; procedure TCCColorPart.PostWrite(var Context: TCCWriting; Start: TCoord); begin SetColors; end;
As already mentioned in the mechanics there’s a root part class – TCCPart – which all other parts inherit from. This section provides a brief review of their methods.
NB: the structure of the class tree is not perfect and there are things I would change (e.g. remove InheritsFrom checks from TCCParts.Parse). However, it works and the issues are small enough to cope with them for now.
Base class for all ColorConsole formatted string parts.
pascalprotected // initializes the object; used in place of Create since there is no // virtual TCCPart.Create because there can be no default parameter list: procedure Init; virtual; // Sends a string to the Writer to be output: procedure WriteStrTo(var Context: TCCWriting; const Str: WideString); public // returns an initialized object if this class can handle part // appearing at current Context.Pos: class function TryParsing(var Context: TCCParsing): TCCPart; virtual; // used by repeaters - normally when a repeater follows a text string // it will cut the last symbol from it instead of repeating all of it: function PrecedingTextLength: Integer; virtual; // if returns False this part is not a text part and won't be split // by the repeater following it: function Splitable: Boolean; virtual; // returns plain text version of this part; note that it's only accurate // for text-only parts and others (repeaters, etc.) might be rough. function PlainText: WideString; virtual; // outputs this part: procedure Write(var Context: TCCWriting); virtual; // if returns True this and previous parts will be Write'n again: function Repeats(var Context: TCCWriting; Iterations: Integer): Boolean; // is called after an iteration has ended (no repeats left): procedure ExitWrite; virtual;
Represents a plain text string. Methods of interest:
pascalpublic constructor Create(const Text: WideString); // keeps MaxLength chars of Text for self and returns a new text object // with Text set to the rest: function Split(MaxLength: Integer): TCCTextPart;
Represents a collection of more string parts. Methods of interest:
pascalprotected // True if this object needs closing "}", False if it must end // on EOF: FNeedsCloser: Boolean; // child parts, values are TCCPart objects: FParts: TObjectList; // original substring that was Parse'd: FOriginal: WideString; // picks up the context and parses string until this group ends: procedure Parse(var Context: TCCParsing); virtual;
Represents a part repeating preceding part. Method of interest:
pascalprotected // raises a ECCParsing exception if this part is #1 in its // parent part collection: procedure EnsureFollowsAnyPart(const Context: TCCWriting);
Download ColorConsole unit for Delphi 7.
Just a few of my thoughts, the main of which has been recurring over and over lately…
I mean, I like Delphi very much. More than C++ anyway. But come on – ColorConsole has started from two functions 175 lines in total – and now look, it’s whole lot 1370 lines (or 1025 without blanks)! And it’s not the first time this happens.
Roughly 270 lines belong to the interface part which contains no code but still needs to be maintained (and class definitions need to be synced with the implementation). This is approximately 20% of the entire unit – doesn’t sound like much but somehow it still seems bulky. Not really convenient.
Perhaps the problem isn’t in the amount of lines itself, though. Perhaps the problem is that it’s a native language instead of concise scripts or functional language codes. Or maybe it’s because of useful editor commands (code suggestions, class autocompletion, jump to declaration and back, etc.) – they don’t work if interface section is wrong or out of sync or if there are syntax errors in implementation and since them don’t work you can’t fix the errors either.
This elusive component feels like sand in the eye for me – doesn’t bother you much but you can’t really do anything.