Introduction
After watching many of Daniel Shiffman's videos on YouTube (The Coding Train), I was inspired to create a tool similar to a Java based tool he uses, but for .NET developers. I started with the project early in 2017, and after several iterations, recently "launched" the project. A very basic introductory video is also available for more information.
I hope that SketchIt will serve two main purposes:
- that it will be used as a tool to learn coding
- that it will be used to create fun and creative projects with
Please note, this is a first of many for me:
- first article of any kind on any platform
- first open source project
- first YouTube video
- first GitHub repository
- possibly some others that I can think of now...
I'll appreciate your feedback.
IMPORTANT: A lot of functionality, improvements and optimizations are still outstanding, please get involved on GitHub.
Background
The whole idea behind SketchIt started back in 2016. I was watching a documentary series on Netflix (Marcus du Sautoy, The Secret Rules of Modern Living: Algorithms), and was again fascinated by the Traveling Salesman Problem (TSP). I wrote some code to see how I could solve a TSP route using brute force, and played with some other ideas - all visually in a WinForms application. This was already a trigger for me - why do I need to create a whole new solution in Visual Studio every time I want to quickly test something or play around with an idea.
Then, one day, YouTube suggested a video by Daniel Shiffman about the Travelling Salesman Problem - this was the first time I watched any of his videos, the first of many. After watching some of his videos, I noticed the tool mentioned at the beginning of this article, called Processing, a Java based development environment. And so, SketchIt was born.
The IDE
One of my goals was to keep things plain and simple, and hopefully that is what you'll notice when you open SketchIt:
The main components are:
- The Menu and Toolbar
- The Code Editor
- An Error List
- A Live Preview Pane
- At the top, the canvas
- At the bottom, the console window
- The Statusbar
SIDE NOTE: You can modify the color scheme of the IDE by editing the settings.json file in the %APPDATA%\SketchIt folder. The above screen is a modified color scheme, below is the default scheme:
Some IDE issues to be completed/improved include:
- Intellisense (the current implementation is not very intelligent)
- Code auto-completion
- Code auto-formatting (indentation, etc.)
- Member tooltips
A Simple Example using SketchIt
When opening SketchIt, you are presented with a code editor. SketchIt can be used in "static" mode, or "animation" mode. In static mode, a block of code is executed sequentially from top to bottom, and the program (or "sketch") comes to an end. Here is a basic example that can be copied and pasted directly into the SketchIt editor:
float value = Random() * 100;
PrintLine("The value between 0 and 100 is", value);
From the code snippet above, you can see that no methods are declared. You simply enter a block of code and it executes (you can enable a "Live Preview" of your code using the Live Preview toolbar button).
Using animation mode is just as simple. The following code can again be pasted into the SketchIt editor:
void Setup()
{
SetSize(800, 400);
}
void Draw()
{
DrawBackground(0);
DrawEllipse(Random(Width), Random(Height), Random(10, 50));
}
To make use of the animation loop, a Draw()
method is required - this is the animation loop. An optional Setup()
method can be used to initialize variables, etc., and also to set up the canvas. The above code sets the canvas size to 800 x 400 pixels. Then in the animation loop, the background is cleared using the DrawBackground()
function, and an ellipse is drawn at a random location (based on the canvas size) with a random diameter of between 10 and 50 pixels.
Challenges
While developing SketchIt, I wanted to have some features and functions available from the get go, some proved more challenging than others (and some are, of course, still outstanding):
- An API to host SketchIt functionality in other applications
- How will I compile the code entered into the text editor? How can functions be invoked without a qualified name?
- How will I extract variable/member information from the user's code?
- A live preview of the sketch as the code is modified
- A proper text editor, with all the bells and whistles expected from a modern editor (this point needs some attention)
- A 3D renderer (this point is still outstanding)
- Available on Mac/Linux (this point is still outstanding)
- And some other, minor challenges
An API to Host SketchIt Functionality in Other Applications
I wanted to make sure that a sketch could be implemented in an external application, without modifying too much of the code as used in the SketchIt IDE. To accomplish this, I created an API which can be referenced externally. Here's a truncated version of the splash screen animation of SketchIt. The animation was created using the SketchIt IDE, then incorporated into a form:
public class SplashAnimation : Sketch
{
void ClearBackground()
{
bg.DrawBackground(GetColor(ControlPaint.Dark(AppearanceSettings.BaseBackColor, .9f)));
}
void Draw()
{
foreach (Dot pt in points)
{
pt.Update();
}
DrawBackground(bg);
SetTextAlignment(Constants.LEFT, Constants.TOP);
SetStroke(GetColor(AppearanceSettings.ApplicationBackColor));
SetFont("Segoe UI Light", 40, false, false);
SetFont("Segoe UI Light", 10, true, false);
SetStroke(GetColor(ControlPaint.Light(AppearanceSettings.ApplicationTextColor, .9f)));
SetFont("Segoe UI Light", 40, false, false);
DrawText("SketchIt", 5, 10, Width - 10, Height - 20);
}
void KeyPress()
{
if (!IsLooping)
{
ClearBackground();
a = 10;
Loop();
}
}
}
And here is the (truncated) code of the splash screen animation window:
public partial class SplashScreenForm : BaseForm
{
private SplashAnimation _animation;
private void SplashScreenForm_Load(object sender, System.EventArgs e)
{
_animation = new SplashAnimation();
_animation.Start(ctlCanvas);
Size = new Drawing.Size(_animation.Width, _animation.Height);
CenterToScreen();
ActiveControl = ctlCanvas;
}
protected override void OnPaint(PaintEventArgs e)
{
using (Drawing.Pen pen = new Drawing.Pen(AppearanceSettings.ActiveCaptionBackColor, 4))
{
e.Graphics.DrawRectangle(pen, 0, 0, Width, Height);
}
}
}
The first thing to note is the Sketch
inheritance of the SplashAnimation
class. The Sketch
class contains (almost) all functions available from the SketchIt
IDE. Looking at the SplashAnimation
class above, you'll note that constants are prefixed with "Constants.
". This is typically something I wanted the user to not care about while using the SketchIt IDE, especially when someone is new to coding. As little prefixing as possible. But, now that the code is used outside the IDE, we prefix it (though avoidable using the "using static
" directive).
The SketchIt
API currently consists of the following namespaces:
SketchIt.Api
SketchIt.Api.Interfaces
SketchIt.Api.Renderers
SketchIt.Api.Static
The Sketch
class found in the SketchIt.Api namespace
is an important class and takes care of a lot of things, such as:
- Invoking the
Setup()
and Draw()
methods - if they do exist - Keeping track of the draw timer to try and maintain the required frame rate
- Notifying the sketch container to update after an animation loop has completed
The Sketch
class also implements a number of interfaces found in the SketchIt.Api.Interfaces namespace
. For example, drawing to the canvas is not handled by the Sketch
class, but by the Renderer
class. But to allow the user to use something like DrawLine(0, 0, Width, Height)
instead of Renderer.DrawLine(0, 0, Width, Height)
, the Sketch
class implements the IRenderer
interface, and then calls the corresponding function of the instantiated renderer. The same goes for style information (such as ColorMode
) which is handled by the Style
class. To allow the user to use SetColorMode(HSB)
instead of CurrentLayer.Style.SetColorMode(HSB)
, the IStyle
interface is implemented and mapped to the corresponding Canvas.Style
member.
Although not part of the SketchIt
API, there is another interesting class found in the SketchIt.Windows
assembly. The SketchIt.Windows.Application
class is basically a mapping to the Sketch
class, but with static
versions of all applicable members. The SketchIt.Windows.Application
class is in essence the Sketch
class implementation for the SketchIt
IDE.
How Will I Compile the Code Entered into the Text Editor? How Can Functions be Invoked Without a Qualified Name?
Now this point was pretty interresting. I originally used the standard CSharpCodeProvider
class to compile code, and all went well, until I realized I was unable to compile code using the using static
directive (which was only introduced in C# 6). From what I can gather, the standard CSharpCodeProvider
cannot compile C# 6 code, and that is when I switched to Roslyn (Microsoft.CodeDom.Providers.DotNetCompilerPlatform
). This might sound a bit melodramatic, but it was quite a game changer for me. The using static
directive solved a big requirement I had: using the SketchIt IDE, the user should be able to create multiple classes in multiple files, and never have to use qualified names for SketchIt functions. The only way I was able to accomplish this was with the using static
directive.
The IDE compiles the code by doing the following:
- Concatenate all source files into a single
string
- Check if the user's code is a sketch running in "static" mode or "animation" mode
- Pull the concatenated
string
into a template which is used to compile the code
The template looks like this:
using System;
using System.Collections;
using System.Collections.Generic;
using SketchIt.Api;
using static SketchIt.Windows.Application;
using static SketchIt.Api.Static.Functions;
using static SketchIt.Api.Static.Constants;
public partial class App : Applet
{
}
Note the use of the using static
directive. This allows us to reference SketchIt.Windows.Application
, SketchIt.Api.Static.Functions
and SketchIt.Api.Static.Constants
members without any qualified names. Problem: Solved. From the template above, the //#code-place-holder#
and //#using-place-holder
string
s are replaced with the user's code. Let's have a look at how this is done:
public bool Compile()
{
Status status = IsBackgroundCompiler ? null : Status.Set("Compiling...");
CompilerErrors = null;
Output = null;
Exception = null;
try
{
string outputFolder = Settings.GetUserFolder() + "\\temp";
string startup = Properties.Resources.AppStartup;
SourceCode[] sourceCode = GetCodeFiles();
List<string> sourceFiles = new List<string>();
bool isStatic = Program.Parser.IsStatic();
if (!Directory.Exists(outputFolder))
{
Directory.CreateDirectory(outputFolder);
}
sourceFiles.Add(startup);
This above code is pretty straightforward:
- Change the status of the application
- Reset and initialize some variables
- Get all the source code
- Determine if this is a "
static
mode" sketch (the Parser.IsStatic()
function simply checks if any methods are declared in the user's code. If not, we assume static
mode) - Create the output folder if it does not exist
int usingOffset = Properties.Resources.AppTemplate.IndexOf("//#using-place-holder#");
int codeOffset = Properties.Resources.AppTemplate.IndexOf("//#code-place-holder#");
usingOffset = Properties.Resources.AppTemplate.Substring(0, usingOffset).Split(new string[]
{ Environment.NewLine }, StringSplitOptions.None).Length - 1;
codeOffset = Properties.Resources.AppTemplate.Substring(0, codeOffset).Split(new string[]
{ Environment.NewLine }, StringSplitOptions.None).Length - 1;
foreach (SourceCode sc in sourceCode)
{
sourceFiles.Add(Properties.Resources.AppTemplate
.Replace("//#using-place-holder#", sc.Using)
.Replace("//#code-place-holder#", (isStatic ? "void RunStatic() {" : "") +
sc.Code + (isStatic ? "\r\n}" : ""))
);
}
Then, from the code above, we do the following:
- Get the location of the two place holders within the code template (the "
//#....-place-holder#
" string
s) - For each code file, we replace the place holders with the applicable code (
using
directives must always appear at the top of the user's code - we then move it to the appropriate spot in the template)
List<string> assemblyList = new List<string>(new string[] {
"System.dll",
"System.Windows.Forms.dll",
"System.Drawing.dll"
});
assemblyList.AddRange(GetAssemblyList());
Dictionary<string, string> options = new Dictionary<string, string>();
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters parameters = new CompilerParameters(assemblyList.ToArray())
{
GenerateInMemory = IsBackgroundCompiler,
GenerateExecutable = !IsBackgroundCompiler,
OutputAssembly = outputFolder + (IsBackgroundCompiler ? "\\sketch.tmp" : "\\sketch.exe"),
CompilerOptions = "/target:winexe /optimize /win32icon:\"" +
Application.StartupPath + "\\SketchIt.ico\"",
};
Next, we add the reference assemblies, and initialize the compiler parameters.
int retries = 0;
while (File.Exists(parameters.OutputAssembly) && retries < 10)
{
try
{
File.Delete(parameters.OutputAssembly);
}
catch
{
retries++;
}
}
We then try to delete an existing output if it already exists (we allow 10 retries since it might be in use).
if (!IsBackgroundCompiler)
{
if (!File.Exists(Application.StartupPath + "\\SketchIt.ico"))
{
using (FileStream stream = new FileStream
(Application.StartupPath + "\\SketchIt.ico", FileMode.Create))
Program.MainForm.Icon.Save(stream);
}
parameters.EmbeddedResources.Add(Application.StartupPath + "\\SketchIt.ico");
foreach (string filename in GetAssemblyList(true))
{
parameters.EmbeddedResources.Add(filename);
}
foreach (ILibrary library in Program.Libraries)
{
FileInfo libraryFile = new FileInfo(library.GetType().Assembly.Location);
List<string> dependancies = new List<string>(library.AdditionalDependancies);
if (!library.Embeddable)
{
dependancies.Add(libraryFile.Name);
}
foreach (string dependancy in dependancies)
{
FileInfo destinationFile = new FileInfo(outputFolder + "\\" + dependancy);
string sourceFile = libraryFile.DirectoryName + "\\" + dependancy;
if (!Directory.Exists(destinationFile.DirectoryName))
{
Directory.CreateDirectory(destinationFile.DirectoryName);
}
if (File.Exists(destinationFile.FullName))
{
try
{
File.Delete(destinationFile.FullName);
}
catch
{
}
}
if (!File.Exists(destinationFile.FullName))
{
File.Copy(sourceFile, destinationFile.FullName);
}
}
}
}
If this is not a background compiler, we do a couple of things:
- Make sure the application icon exists to embed into the assembly
- Check if referenced libraries can be embedded into the assembly (this includes the
SketchIt
API) - For non-embeddable libraries, we copy the dependancies to the output folder
CompilerResults results = provider.CompileAssemblyFromSource(parameters, sourceFiles.ToArray());
if (results.Errors.Count == 0)
{
Output = results.CompiledAssembly;
}
else
{
foreach (CompilerError error in results.Errors)
{
try
{
FileInfo fileInfo = string.IsNullOrEmpty(error.FileName) ?
null : new FileInfo(Path.GetFileNameWithoutExtension(error.FileName));
int fileIndex = fileInfo != null ? int.Parse(fileInfo.Extension.Substring(1)) - 1 : -1;
if (fileIndex < 0)
{
}
else if (error.Line < codeOffset + sourceCode[fileIndex].UsingLineCount)
{
error.Line -= usingOffset;
}
else if (error.Line - codeOffset > sourceCode[fileIndex].CodeLineCount)
{
error.Line = sourceCode[fileIndex].CodeLineCount;
}
else
{
error.Line -= codeOffset;
}
}
catch (Exception ex)
{
throw ex;
}
}
CompilerErrors = results.Errors;
}
}
catch (Exception ex)
{
Exception = ex;
}
finally
{
if (status != null)
{
status.Dispose();
}
}
return Output != null;
Then, we compile the assembly:
- If an exception is thrown during compilation, the
catch
block stores the thrown exception - If no exception is thrown, and the compilation is successful, we set the
Output
of the compiler to the CompiledAssembly
- If the compiler encountered errors, we update the line number to correspond with the IDE line numbers
- Finally, we return
true
if the compilation was successful, false
otherwise
How Will I Extract Variable/Member Information from the User's Code?
I started off with a small little parser of my own, but after incorporating Roslyn, made use of the CSharpSyntaxTree
class to parse the code. This is also something that needs more attention as it goes hand-in-hand with the autocompletion functionality.
CSharpParseOptions options = new CSharpParseOptions
(LanguageVersion.Default, DocumentationMode.None, SourceCodeKind.Script);
SyntaxTree = CSharpSyntaxTree.ParseText(input.ToString(), options);
_compilation = CSharpCompilation.Create("Sketch", new SyntaxTree[] { SyntaxTree });
_semanticModel = _compilation.GetSemanticModel(SyntaxTree, true);
ParsedTypes = "";
ParsedKeywords = "";
foreach (SyntaxNode node in SyntaxTree.GetCompilationUnitRoot().DescendantNodes())
{
if (node is MethodDeclarationSyntax method)
{
ParsedKeywords += method.Identifier.ValueText + " ";
}
else if (node is FieldDeclarationSyntax field)
{
}
else if (node is ClassDeclarationSyntax cls)
{
ParsedTypes += cls.Identifier.ValueText + " ";
}
else
{
node.ToString();
}
}
After calling CSharpSyntaxTree.ParseText()
, the SyntaxNode
s are enumerated, and an updated list of keywords and types created. The UpdateEditor()
method of all open EditorForm
windows is then invoked, which in turn updates the Scintilla editor:
for (int i = 0; i < Application.OpenForms.Count; i++)
{
if (!(Application.OpenForms[i] is BaseForm form) || form.Type != WindowType.SourceFile) continue;
form.Invoke(new MethodInvoker(((EditorForm)form).UpdateEditor));
}
internal void UpdateEditor()
{
Editor.SetKeywords(1, Program.Parser.KnownKeywords + " " + Program.Parser.ParsedKeywords);
Editor.SetKeywords(3, Program.Parser.KnownTypes + " " + Program.Parser.ParsedTypes);
}
A Live Preview of the Sketch as the Code is Modified
I was very excited to implement this feature. While developing SketchIt, and testing functions available (from within the IDE), it quickly became a boring task to make a change in the API, then launch the IDE, write or paste some code to test a function, then compile to see the resulting behaviour (Edit and Continue is not applicable in this case, since changes during Edit and Continue will not be included in the compiled output sketch). So implementing the Live Preview function removed the tediousness of testing new functions. Implementing it wasn't too difficult, but it had a couple of challenges:
- Compile the code in the background without blocking the IDE
- Once the code is compiled, update the preview pane
- Stretch/Scale the canvas to fit inside the preview pane (a fullscreen stretch introduced some issues)
- Cross-thread issue while updating the preview pane
- Making the preview pane interactive (respond to mouse/keyboard events)
The EditorForm
checks when text inside the code editor changes, and if so, notifies the MainForm
:
private void EditorTextChanged(object sender, EventArgs e)
{
EditorText = Editor.Text;
Program.MainForm.CodeChanged(this);
}
The MainForm
then resets a timer which starts the background compiler (we do not compile every time the text changes, we do allow a few milliseconds before doing so):
private void PreviewTimerElapsed(object sender, EventArgs e)
{
_previewTimer.Stop();
BuildPreview();
}
And the BuildPreview
method kicks off a BackgroundWorker
to compile the code:
private void BuildPreview()
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
worker.RunWorkerAsync();
}
Code is compiled in the background using the BackgrounWorker
class by invoking the Compile
function mentioned in the previous point. For the live preview pane, once compilation has completed, the existing (if any) live preview is disposed, and a new instance of the compiled assembly is instantiated and the live preview is updated. Many cross-thread exceptions were resolved implementing a ThreadLocker
class within the API. Inside the API, a number of operations are wrapped inside a using (ThreadLocker...)
code block, though, I think this approach needs some revisiting. The interactive preview pane also introduced cross-thread issues, but the ThreadLocker
class helped solve these issue. Scaling of the sketch also had to be accounted for in the interactive preview pane.
Apart from the minor obstacles implementing this feature, I found it very useful once implemented. I one day sat with my youngest son (12 year old), who isn't into programming (at the moment), and once he saw that when he changes something in the code editor, it immediately reflects in the preview pane, he was more excited about trying out some code.
A Proper Text Editor, with All the Bells and Whistles Expected from a Modern Editor
Some improvements are in order, but for the time being, what we've got is workable. Thanks to ScintillaNET, this wasn't too difficult to implement. Looking up the Scintilla documentation is pretty simple. The autocomplete functionality in Scintilla is very basic, so an alternative solution needs to be implemented for this. The SketchIt editor inherits the Scintilla
class:
public class EditorControl : Scintilla
{
private string _keywordList = "class extends implements import ...(removed for article purposes)";
public EditorControl()
{
string list = "";
foreach (string word in _keywordList.Split(new char[] { ' ' }))
{
if (list.IndexOf(word + " ") > -1) continue;
list += word + " ";
}
_keywordList = list.Trim();
Lexer = Lexer.Cpp;
BorderStyle = System.Windows.Forms.BorderStyle.None;
UsePopup(PopupMode.All);
AutoCIgnoreCase = true;
AutoCMaxHeight = 15;
AutoCOrder = Order.PerformSort;
UpdateAppearance();
The editor checks keystrokes, and tries to show autocomplete popups when applicable based on the current context in the code, e.g.: when the period character is added, a list of members should appear:
case '.':
string[] members = Program.Parser.GetMembers(CurrentPosition - 1);
if (members.Length > 0)
{
string list = " ";
foreach (string member in members)
{
if (list.IndexOf(" " + member + " ") == -1)
{
list += member + " ";
}
}
AutoCShow(0, list.Trim());
}
A 3D Renderer
This feature is still outstanding. I have started playing with an OpenGL implementation using the SharpGL library, but it is incomplete - my knowledge on OpenGL is just too limited at the moment.
Available on Mac/Linux
Completely outstanding. I hope that some developers will get involved and help to make this possible.
And Some Other, Minor Challenges
I wanted to give the IDE a bit more of a modern look and feel, instead of just a plain form with a text editor. For the menus and toolbars, it was easily done implementing a custom ToolStripRenderer
. Other controls such as TextBox
or ComboBox
controls are wrapped with a class I created called VisualWrapper
. This class does nothing more really than changing borders and colors, depending on which control is active. A very basic TabButtonsControl
class has been created for the "tabbed windows". Overall, I think the results are satisfying for now.
I'm also making use of the FileSysemWatcher
class to monitor changes to the settings.json file. When this file changes, the appearance settings are reapplied (the application does not need to be restarted). Something small, but it makes things a little more user friendly.
Finally
This article is purely a brief introduction to SketchIt, and hopefully enough to get some of you interested in using it, or even getting involved with the development. I simply do not have enough time to take this project to the next level all by myself. Apart from the IDE issues mentioned above, I would also like to:
- Implement a 3D renderer. I have played around with an OpenGL implementation using the SharpGL library, but my OpenGL knowledge is close to non-existent.
- Make SketchIt available on platforms other than Windows.
- Many, many more...
Here is a list of libraries/tools/icons used to create SketchIt:
- ScintillaNET for the text editor
- Roslyn for code compilation and analysis
- Emgu.CV for the video capture library
- Newtonsoft.Json to read/write JSON data
- Sandcastle Help File Builder GUI to build the help file
- Icons8 for the majority of the icons
- Microsoft Visual Studio Community 2017
- And a couple of functions/code snippets from the web which is noted in the source code
Download and Links
History
- 2018-08-03: Initial article