Update
This project has been updated with new features. The image illustrates the new GUI.
Features include:
- Open a whole folder as a project
- Add multiple projects
- Save all opened files at once
- Compile the currently selected file
- Create new folders and files in selected project
- View image files
Introduction
Lately I've been trying to brush up on some of my Java. It's been some time since I wrote a piece ofJava code. I downloaded the latest Java SDK and began writing some basic code in Notepad and used the command prompt to execute my programs. This worked well until I began working with more Javafiles. I found myself using more command prompts and this just got out of control. Now you're probably wondering why I didn't just download a Java editor such a NetBeans or Eclipse. Well my poor laptop is running various applications and using enough of my limited memory as it is, I didn't want to install another bulky IDE that I would use rarely. I was about to do a Google search for a lightweightJava editor when I was hit with a light bulb moment. I thought about developing my own editor. I mean how hard could it be?
Design
I began to design the application and like most developers I did this in my head. After all it’s a very simple editor. My first attempt at designing the UI consisted of the following controls:
MenuStrip
– To hold menus such as New, Open fileTabControl
– To containRichTextBox
to create/edit files
With the design complete, I began implementing the code.
Code
I removed the default TabPages from the TabControls collection. I would add a new TabPage programmatically when the New File menu was clicked. Listing 1.0 below shows the code for the
NewFile()
method.Listing 1.0
Hide Copy Code
public void NewFile()
{
string Filename = "Untitled" + (FileTabControl.TabCount + 1);
TabPage Page = new TabPage();
Page.Name = Filename;
Page.Text = Filename;
RichTextBox Editor = new RichTextBox();
Editor.AcceptsTab = true;
Editor.Dock = DockStyle.Fill;
Page.Controls.Add(Editor);
FileTabControl.TabPages.Add(Page);
}
The above method creates a
TabPage
with a RichTextBox
control. By default, all TabPage
s are titled "Untitled
" followed by a number which is the total tab count.
With the
NewFile()
method implemented, it was time to move onto the SaveFile()
method, another easy implementation I thought. The SaveFile()
method would get the selected tabs text from the RichTextBox
control and show the SaveFileDialog
. Getting the text from theRichTextBox
control was easy as all I had to do was use the ControlCollection
class to get all controls placed in the TabPage
. This was done using a simple foreach
loop.
After getting the text from the
RichTextBox
control, I displayed the SaveFileDialog
and using an instance of the StreamWriter
class, I managed to save the file.
Happy with my code so far, I ran my project and everything worked fine. However I realised that if I wanted to save my updated file and clicked on the
SaveFile
menu, the SaveFileDialog
would show again and prompt me to save the file even though it had been previously saved.
What I needed was a way to determine if a file was previously saved so I can take the appropriate action, either save the file for the first time or override the existing file.
This was my first problem and I decided I would deal with it quickly by created a custom class called
FileObject
. Listing 1.1 below shows the FileObject
class.Listing 1.1
Hide Shrink Copy Code
public class FileObject
{
private bool _SaveState = false;
private string _FilePath;
private string _FileName;
public bool SaveState
{
get
{
return this._SaveState;
}
set
{
this._SaveState = value;
}
}
public string FilePath
{
get
{
return this._FilePath;
}
set
{
this._FilePath = value;
}
}
public string FileName
{
get
{
return this._FileName;
}
set
{
this._FileName = value;
}
}
}
What does this class have to do with my problem you ask? Well the
TabPage
class has a property called Tag
. You can use this property to get
/set
an object. By supplying this Tag
with an object that held information about the current TabPage
, I could determine if a selected TabPage
was previously saved by checking the _SaveState
variable in the FileObject
class.
This meant I would have to modify the
NewFile()
method and set the Tag
property of the TabPage
using an instance of the FileObject
class. When I save the file, I can get the FileObject
from theTag
property and check if the file was previously saved. Listing 1.2 below shows the modifiedNewFile()
and listing 1.3 shows the SaveFile()
method.Listing 1.2
Hide Copy Code
public void NewFile()
{
string Filename = "Untitled" + (FileTabControl.TabCount + 1);
FileObject FileObj = new FileObject();
FileObj.SaveState = false;
FileObj.FileName = Filename;
TabPage Page = new TabPage();
Page.Name = Filename;
Page.Text = Filename;
Page.Tag = FileObj;
RichTextBox Editor = new RichTextBox();
Editor.AcceptsTab = true;
Editor.Dock = DockStyle.Fill;
Page.Controls.Add(Editor);
FileTabControl.TabPages.Add(Page);
}
Listing 1.3
Hide Shrink Copy Code
public void SaveFile()
{
TabPage Page = FileTabControl.SelectedTab;
Control.ControlCollection TxtCollection = Page.Controls;
string FileData = "";
foreach (RichTextBox Editor in TxtCollection)
{
FileData = Editor.Text;
}
FileObject FileObj = (FileObject)Page.Tag;
if (FileObj.SaveState == false)
{
SaveFileDialog SaveDialog = new SaveFileDialog();
SaveDialog.Filter = "Java|*.java";
DialogResult SaveResult = SaveDialog.ShowDialog();
if (SaveResult == DialogResult.OK)
{
string FileName = SaveDialog.FileName;
string FilePath = Path.GetFullPath(FileName);
FileObj.SaveState = true;
FileObj.FilePath = FilePath;
FileObj.FileName = FileName;
StreamWriter FileWriter = new StreamWriter(FilePath);
FileWriter.Write(FileData);
FileWriter.Flush();
FileWriter.Close();
FileWriter.Dispose();
Page.Tag = FileObj;
Page.Text = Path.GetFileName(FileName);
}
}
else
{
string FilePath = FileObj.FilePath;
StreamWriter FileWriter = new StreamWriter(FilePath);
FileWriter.Write(FileData);
FileWriter.Flush();
FileWriter.Close();
FileWriter.Dispose();
}
I tested my application again and this time a previously saved file was saved again without showing the
SaveFileDialog
.
Now I can create new files and save them. All that is needed now is to compile and run the files. This is where the
Process
class is introduced. I used this class to run a process. This process simply executed the Javac and Java executables files with a filename as its argument. All I had to do was create a Compile button and run these processes.
But wait, what file would I be compiling? The currently selected file? Would I need to save the file first before compiling? So many questions and very quickly problems started occurring.
To solve this problem, I thought of how Visual Studio compiles a program. When you click the Run button in Visual Studio, the project is saved and Visual Studio tries to compile the program.
I decided that I didn't want to have to save all the files individually before compiling a file. I wanted the Compile button to loop through all the
TabPage
s and save each file. As each file is being saved, a new process should be executed which should try and compile the file. The output from the process should be captured and displayed in a textbox
.
This meant I had to modify the UI to show the output from the process. I added two new controls to the form:
SplitContainer
TabControl
(OutputTabContainer
) contained withinPanel2
of theSplitContainer
I copied the existing
TabControl
and pasted it into Panel1
of the SplitContainer
control. TheOutputTabContainer
consists of two TabPage
s (ConsolePage
and ErrorPage
). TheConsolePage
shows any output from the process which is not an error whilst the ErrorPage
shows any output from the process which is an error.
With the UI controls in place, I began to work on the code for the Compile button. I created three methods (
Compile
, Javac
and Java
). The Compile
method is responsible for creating a new process, it takes three arguments and returns a Boolean indicating whether the process started successfully. Listing 1.4 below shows the Compile()
method.Listing 1.4
Hide Copy Code
public bool Compile(string EXE, string WorkingDirectory, string FileName)
{
_Compiler.StartInfo.FileName = EXE;
_Compiler.StartInfo.Arguments = FileName;
_Compiler.StartInfo.WorkingDirectory = WorkingDirectory;
_Compiler.StartInfo.CreateNoWindow = true;
_Compiler.StartInfo.ErrorDialog = false;
_Compiler.StartInfo.UseShellExecute = false;
_Compiler.StartInfo.RedirectStandardOutput = true;
_Compiler.StartInfo.RedirectStandardError = true;
bool processStarted = _Compiler.Start();
return processStarted;
}
The first argument is the executable file to launch, the second argument is the working directory of theJava file and the third is the file name of the Java file. The working directory argument is needed as there may be Java files in different directories.
The
Compile()
method is called from within the Javac()
and Java(
) methods. The Javac()
method calls the Compile()
method and provides the Compile()
method with its arguments. It is also responsible for capturing the output from the process started by the Compile()
method. TheJavac()
method only checks for output errors, this is when a Java file cannot compile for any particular reason, the output error is displayed in the ErrorPage TabPage
.
Remember that the
Javac()
method is called for every TabPage
. If there are any output errors, a global Boolean variable (_CompileProgram
) is set to false
. If there were no output errors, the variable is set to true
.
By setting this variable, the Compile button can determine whether to move to the next phase of the compiling process, which is to execute a file. Listing 1.5 below shows the
Javac()
method.Listing 1.5
Hide Copy Code
public void Javac(string FileName)
{
string FullPath = Path.GetFullPath(FileName);
int LastIndex = FullPath.LastIndexOf('\\') + 1;
string WorkingDirectory = FullPath.Substring(0, LastIndex);
if (this.Compile(_JavacPath, WorkingDirectory, Path.GetFileName(FileName)))
{
_ErrorReader = _Compiler.StandardError;
_ErrorOutput = _ErrorReader.ReadToEnd();
if (_ErrorOutput != "")
{
ErrorOutput.AppendText(_ErrorOutput);
this._CompileProgram = false;
}
else
{
this._CompileProgram = true;
}
}
}
Only when the global variable
_CompileProgram
is set to true
is when the Java()
method is called. This method is only called once unlike the Javac()
method which is called for every TabPage
. The reason for this is because, not every Java file will contain a main
method. As you know, a main()
method is required in many programming languages as it is the main point of program execution.
The Tiny Java Editor may consist of files which do not have a
main()
method. For this reason, we cannot call the Java()
method for every TabPage
. So how then do we execute a Java file.
I decided to place a
ComboBox
control on the form which would contain a list of all the files. This list is populated every time a new file is added or saved. Using this ComboBox
, I can select a single file, which would be a Java file that consists of a main()
method. Listing 1.6 below shows the Java()
method:
Listing 1.6
Hide Copy Code
public void Java(string FileName)
{
string FullPath = Path.GetFullPath(FileName);
int LastIndex = FullPath.LastIndexOf('\\') + 1;
string WorkingDirectory = FullPath.Substring(0, LastIndex);
if (Compile(_JavaPath, WorkingDirectory, Path.GetFileNameWithoutExtension(FileName)))
{
_OutputReader = _Compiler.StandardOutput;
_ConsoleOutput = _OutputReader.ReadToEnd();
ConsoleOutput.Text = _ConsoleOutput;
}
}
Notice in the above code, the StandardOutput from the _Compiler process is being retrieved unlike theJavac() method which was getting the StandardError. By now the project was complete. I was able to create/save files as well as compile them.
Highlighting Line With Error
In the middle of a tea break, I though wouldn't it be great to highlight the line an error occurred. The output from the StandardError contains the filename and the line number of the error. All I had to do was get a reference to the RichTextBox control, find the line and highlight it. I modified the Javac() method to reflect the code in Listing 1.7.
Listing 1.7
Hide Shrink Copy Code
public void Javac(string FileName, RichTextBox Editor)
{
string FullPath = Path.GetFullPath(FileName);
int LastIndex = FullPath.LastIndexOf('\\') + 1;
string WorkingDirectory = FullPath.Substring(0, LastIndex);
if (this.Compile(_JavacPath, WorkingDirectory, Path.GetFileName(FileName)))
{
_ErrorReader = _Compiler.StandardError;
_ErrorOutput = _ErrorReader.ReadToEnd();
int ErrorLineNumber = 0;
try
{
string[] ErrorLines = _ErrorOutput.Split('\n');
string[] Error = ErrorLines[0].Split(':');
ErrorLineNumber = Convert.ToInt32(Error[1]) - 1;
}
catch (Exception e) { }
if (_ErrorOutput != "")
{
ErrorOutput.AppendText(_ErrorOutput);
try
{
int FirstCharIndex = Editor.GetFirstCharIndexFromLine(ErrorLineNumber);
int LineLength = Editor.Lines[ErrorLineNumber].Length;
Editor.Select(FirstCharIndex, LineLength);
Editor.SelectionBackColor = Color.Purple;
}
catch (Exception e) { }
this._CompileProgram = false;
}
else
{
this._CompileProgram = true;
}
}
}
Finally I had a Tiny Java Editor, which I could use to create my Java programs. Did my laptop like my Tiny Java Editor? No it didn't. Why? Because the application freezes when you try to execute a Javaprogram with a GUI. The reason for this is because the
_Compile
process should be executed in a different thread and not on the main
thread as it is in this case.
No comments:
Post a Comment