Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Bruce’s Blocks

0.00/5 (No votes)
10 Mar 2019 1  
WPF block-style editor featuring drag-to-reorder and nested levels of grouping

Background

I write sophisticated CNC machine control software for milling, cutting, drilling, EDM, additive, etc. These programs run on a host PC and communicate with multi-axis motion controllers and other industrial devices. They provide touchscreen operator interface, machine control logic, network communications, diagnostics and many other functions. A colorized text editor is usually included for editing g-code.

G-code is the arcane language of CNC programming. Here is a “peck drilling cycle” example:

G83 Z1. F.001 R.03 Q.04 P0

G-code programs are generally created in one of three ways:

  • Generated from a CAD model
  • Generated by a specialized conversational interface
  • Edited in a text editor

CAD generation is highly automated but requires the existence of a CAD model. Conversational interfaces are very application-specific and time consuming to create and maintain. Writing g-code in a text editor is error-prone and requires specialized knowledge.

I have created a block style g-code editor intended to address the shortcomings of these methods. Contact me to discuss licensing the block style g-code editor for use in your CNC applications.

Bruce’s Blocks

As part of the g-code editor development process, I created a much simpler WPF app as a testbed for new techniques such as drag-to-reorder, multi-level grouping, and synchronization between block and text views. This is that app - presented as an open source project!

The primary goal was to refine a block-style text editor with nested levels of grouping. I also wanted to create a clean and modern design aesthetic that eliminates the conventional WPF Window title bar.

Features

  • Manipulate a text file as a collection of graphical blocks
  • Standard editor Ctrl commands
  • Multi-select block cut, copy, delete and grouping
  • Font Awesome Free v5 icons
  • Light and Dark themes
  • Simple .NET 4.6.1 project with no dependencies

Block Manager

The BlockManager class is responsible for managing the collection of supported block types. It is the block factory and provides bi-directional conversion between blocks and text (reading and writing both files and string buffers). The block manager is exposed to the rest of the application via the IBlockManager interface to enable custom versions to be injected at startup.

public interface IBlockManager
{
    /// <summary>The set of block types supported by this manager.</summary>
    IBlock[] BlockTypes { get; }

    /// <summary>Creates an array of new blocks that match the lines.</summary>
    IBlock[] NewBlocks(string buffer);
    /// <summary>Produces a string buffer from an array of blocks.</summary>
    string ToString(IBlock[] blocks);

    /// <summary>Creates an array of new blocks from a text file.</summary>
    bool ReadFile(string path, out IBlock[] blocks);
    /// <summary>Writes a text file from an array of blocks.</summary>
    bool WriteFile(string path, IBlock[] blocks);
}

There are IBlock and IBlockGroup interfaces for the block types and block group types. Each block type manages its own ListBox items for both block view and text view, giving it the freedom to determine how best to display itself. Each block type is capable of creating a new block from a line of text.

public interface IBlock
{
    /// <summary>Parent block group, or null for top-level blocks.</summary>
    IBlockGroup BlockGroup { get; set; }
    /// <summary>Each block maintains its own block and code listbox items.</summary>
    ListBoxItem BlockItem { get; }
    ListBoxItem CodeItem { get; }
    /// <summary>Creates a new block from a line, or null if no match.</summary>
    IBlock NewBlock(string line);
}

The BlockControl class provides the default layout for block view, including an image and three lines of text.

<Grid Height="64" Width="128">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="64" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <Image Name="image" Grid.Column="0" 

    Source="Images/blockPalette.png" VerticalAlignment="Center" />
    <StackPanel Grid.Column="1" Margin="2,0,0,0" VerticalAlignment="Center">
        <TextBlock Name="tb1" Text="Palette" />
        <TextBlock Name="tb2" Text="for" />
        <TextBlock Name="tb3" Text="colors" />
    </StackPanel>
</Grid>

Drag-to-Reorder

The DragPanel class overrides left mouse button events to implement the drag-to-reorder functionality, and overrides MeasureOverride() and ArrangeOverride() to implement its layout strategy. Block types of different sizes are supported. MeasureOverride() sets a uniform row height and column width to accommodate the largest item.

foreach (UIElement child in Children)
{
    child.Measure(infSize);
    itemWidth = Math.Max(itemWidth, child.DesiredSize.Width);
    rowHeight = Math.Max(rowHeight, child.DesiredSize.Height);
}

Each item maintains its list order and layout position via a set of attached properties. A drop shadow effect is added to the selected item when the user starts dragging – making it appear to rise above the other items.

Multi-level Grouping

The BlockList user control holds the block items ListBox. It also includes a horizontal splitter and a lower region that can be populated with a second BlockList upon demand. This arrangement enables an arbitrary number of grouping levels to be displayed. Each BlockList holds references to its parent and child BlockList objects in linked-list fashion.

There is quite a bit of carefully crafted bookkeeping logic behind the MainWindow class that handles the business of implementing a block-style editor (add, delete, group, ungroup, cut, copy, paste, etc.). Nested groups are supported via plenty of recursive method calls!

private void InsertCode(IBlockGroup group, ref int index)
{
    foreach (IBlock block in group.Blocks)
    {
        if (block is IBlockGroup)
            InsertCode((IBlockGroup)block, ref index);
        else
            listCode.Items.Insert(index, block.CodeItem);
    }
}

BlockManager saves the collection of blocks in plain text format, both to files and to the clipboard.

private void WriteLines(StreamWriter sw, IBlock[] blocks, ref int indent)
{
    foreach (IBlock block in blocks)
    {
        for (int i = 0; i < indent; i++) sw.Write("  ");
        sw.WriteLine(block.ToString().Trim());

        if (block is IBlockGroup)
        {
            ++indent;
            IBlockGroup group = (IBlockGroup)block;
            WriteLines(sw, group.Blocks, ref indent);
            --indent;
            for (int i = 0; i < indent; i++) sw.Write("  ");
            sw.WriteLine(BlockGroup.EndMarker);
        }
    }
}

Groups are delimited by start and end markers and group contents are indented for readability.

Custom Window Style

I wanted to implement a simple custom main window without the conventional title bar.

Here is the relevant section of my window style.

<Style x:Key="MainWindowStyle" TargetType="Window">
    <Setter Property="WindowStyle" Value="None" />
    <Setter Property="WindowChrome.WindowChrome">
        <Setter.Value>
            <WindowChrome CaptionHeight="0" GlassFrameThickness="1" ResizeBorderThickness="4" />
        </Setter.Value>
    </Setter>
</Style>

An invisible Button covers the custom title bar region to support window dragging and double-click maximize/restore. The toolbar icons are Font Awesome Free (version 5). The Minimize and Close buttons execute the obvious commands. The only noteworthy trick is in the Maximize/Restore functionality. Setting NoResize before maximizing prevents the maximized window from being positioned slightly off screen.

private void ToggleMaximized()
{
    if (WindowState == WindowState.Maximized)
    {
        WindowState = WindowState.Normal;
        ResizeMode = ResizeMode.CanResize;
    }
    else
    {
        if (WindowState != WindowState.Normal)
            WindowState = WindowState.Normal;
        ResizeMode = ResizeMode.NoResize;
        WindowState = WindowState.Maximized;
    }
}

Light and Dark Themes

The App class holds the global styles and colors in its resource dictionary. App can switch between the Light and Dark themes by simply clearing the dictionary and replacing its contents with the requested color scheme. Remember to specify dynamic resource references in your XAML.

Points of Interest

  • A robust drag-to-reorder WPF ListBox
  • The multi-level block grouping scheme
  • Synchronization between block and text views
  • Support for the set of standard editor functions
  • A simple WPF custom main window implementation
  • Font Awesome Free v5 integration
  • Simple Light and Dark theme support
  • An example of clean, attractive and responsive WPF UI design

I hope you enjoy playing with the app and exploring the code. If you find a bug, then please let me know about it!

History

  • 23rd February, 2019: Initial version
  • 10th March, 2019: The GitHub repository has been updated with a new and improved DragListBox class featuring layout options RowMajor, ColumnMajor, etc. plus many other refinements.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here