htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

Visual Studioに頼らないコーディング

で簡単なテキストエディタを作ってみました。

メモ帳(notepad.exe)だけでどこまで作れるか、みたいな自己満足のチャレンジです。
静的エラーはコンパイラが吐いてくれるから簡単に修正できるけど、実行時エラーの解決はなかなかしんどかったです。
STAThreadAttributeつけないとFileDialogクラスの派生クラスのインスタンス立てるときに死ぬのは勉強になりました。

結果、出来上がったのはメモ帳以下の機能しか持たないクソみたいなエディタでした。

コンパイラのオプションは

csc /target:winexe /o /out:editor.exe editor.cs

です。
複雑なオプションを知らないため、単純化するという目的で1ファイルで完結するようになってます。
お手軽コンパイル


以下コード

using System;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;

public partial class Editor : Form
{
	private const int MaxFiles = 1;
	private readonly Encoding encoding = Encoding.UTF8;
	
	public FileInfo[] FileInfos { get; private set; }
	
	public Editor()
	{
		this.FileInfos = new FileInfo[MaxFiles];
		this.InitializeComponent();
		
		editArea.Size = new Size(this.ClientSize.Width, this.ClientSize.Height - menu.Height);
		editArea.Location = new Point(0, menu.Height);
	}
	
	public Editor(string filePath)
		: this()
	{
		this.OpenFile(filePath);
	}
	
	private void CreateNewFile()
	{
		editArea.Text = string.Empty;
		this.FileInfos[0] = new FileInfo("new file");
		this.ChangeCaption(this.FileInfos[0]);
	}
	
	private void OpenFile()
	{
		using (var ofd = new OpenFileDialog())
		{
			ofd.FileOk += (sender, e) => this.FileInfos[0] = new FileInfo(ofd.FileName);
			DialogResult dr = ofd.ShowDialog(this);
			if (dr != DialogResult.OK) return;
		}
		if (this.FileInfos == null || this.FileInfos[0] == null) return;
		this.OpenFile(this.FileInfos[0]);
	}
	private void OpenFile(string filePath)
	{
		if (File.Exists(filePath))
		{
			using (var sr = new StreamReader(filePath, this.encoding))
			{
				editArea.Text = sr.ReadToEnd();
				editArea.SelectionStart = editArea.SelectionStart;
			}
			this.FileInfos[0] = new FileInfo(filePath);
			this.ChangeCaption(this.FileInfos[0]);
		}
	}
	private void OpenFile(FileInfo fileInfo)
	{
		this.OpenFile(fileInfo.FilePath);
	}
	
	private void SaveFile()
	{
		this.SaveFile(true);
	}
	private void SaveFile(bool overwrite)
	{
		if (!overwrite)
		{
			using (var sfd = new SaveFileDialog())
			{
				sfd.FileOk += (sender, e) => this.FileInfos[0] = new FileInfo(sfd.FileName);
				DialogResult dr = sfd.ShowDialog(this);
				if (dr != DialogResult.OK) return;
			}
		}
		if (this.FileInfos == null || this.FileInfos[0] == null) return;
		if (File.Exists(this.FileInfos[0].FilePath))
		{
			using (var sw = new StreamWriter(this.FileInfos[0].FilePath, false, this.encoding))
			{
				sw.Write(editArea.Text);
				sw.Flush();
			}
			this.FileInfos[0].Changed = false;
			this.ChangeCaption(this.FileInfos[0]);
		}
	}
	
	private void ChangeCaption(FileInfo fileInfo)
	{
		if (fileInfo != null)
		this.Text = fileInfo.ToString();
	}
}


static class Program
{
	[STAThread]
	public static void Main(string[] args)
	{
		Editor editor;
		
		if (args.Length > 0)
		{
			editor = new Editor(args[0]);
		}
		else
		{
			editor = new Editor();
		}
		Application.Run(editor);
	}
}

public class FileInfo
{
	public string FilePath { get; private set; }
	public bool Changed { get; set; }
	
	public FileInfo(string filePath)
	{
		this.FilePath = filePath;
		this.Changed = false;
	}
	
	public override string ToString()
	{
		string symbol = this.Changed ? "*" : string.Empty;
		return symbol + this.FilePath;
	}
}
enum ExitStatus
{
	Success = 0,
	GeneralError = 1,
}

public partial class Editor : Form
{
	private void InitializeComponent()
	{
		menu = new MenuStrip();
		fileMenu = new ToolStripMenuItem();
		fileCreateNewMenu = new ToolStripMenuItem();
		fileOpenMenu = new ToolStripMenuItem();
		fileSaveAsMenu = new ToolStripMenuItem();
		fileSaveMenu = new ToolStripMenuItem();
		fileExitMenu = new ToolStripMenuItem();
		editArea = new TextBox();
		menu.SuspendLayout();
		this.SuspendLayout();
		
		fileMenu.Text = "ファイル(&F)";
		
		fileCreateNewMenu.Text = "新規作成(&N)";
		fileCreateNewMenu.ShortcutKeys = Keys.Control | Keys.N;
		fileCreateNewMenu.Click += (sender, e) => this.CreateNewFile();
		
		fileOpenMenu.Text = "開く(&O)";
		fileOpenMenu.ShortcutKeys = Keys.Control | Keys.O;
		fileOpenMenu.Click += (sender, e) => this.OpenFile();
		
		fileSaveAsMenu.Text = "名前を付けて保存(&A)";
		fileSaveAsMenu.ShortcutKeys = Keys.Control | Keys.Shift | Keys.S;
		fileSaveAsMenu.Click += (sender, e) => this.SaveFile(false);
		
		fileSaveMenu.Text = "上書き保存(&S)";
		fileSaveMenu.ShortcutKeys = Keys.Control | Keys.S;
		fileSaveMenu.Click += (sender, e) => this.SaveFile(true);
		
		fileExitMenu.Text = "終了(&X)";
		fileExitMenu.Click += (sender, e) => this.Close();
		
		editArea.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom;
		editArea.Multiline = true;editArea.AcceptsTab = true;
		editArea.AcceptsReturn = true;
		editArea.Font = new Font("MS Gothic", 10, FontStyle.Regular);
		editArea.WordWrap = false;
		editArea.ScrollBars = ScrollBars.Both;
		editArea.TextChanged += (sender, e) =>
		{
			if (this.FileInfos == null || this.FileInfos[0] == null) return;
			this.FileInfos[0].Changed = true;
			this.ChangeCaption(this.FileInfos[0]);
		};
		
		fileMenu.DropDownItems.AddRange(new ToolStripItem[] {
			fileCreateNewMenu,
			fileOpenMenu,
			new ToolStripSeparator(),
			fileSaveAsMenu,
			fileSaveMenu,
			new ToolStripSeparator(),
			fileExitMenu,
		});
		menu.Items.Add(fileMenu);
		menu.ResumeLayout(false);
		menu.PerformLayout();
		this.Size = new Size(600, 800);
		this.FormClosing += (sender, e) =>
		{
			if (this.FileInfos != null && this.FileInfos[0] != null && this.FileInfos[0].Changed)
			{
				DialogResult dr = MessageBox.Show("終了しますか?", "確認",
					MessageBoxButtons.OKCancel,
					MessageBoxIcon.Question);
				if (dr == DialogResult.OK)
				{
					Environment.Exit((int)ExitStatus.Success);
				}
				else
				{
					e.Cancel = true;
				}
			}
			else
			{
				Environment.Exit((int)ExitStatus.Success);
			}
		};
		this.Controls.AddRange(new Control[] { menu, editArea, });
		this.ResumeLayout(false);
		this.PerformLayout();
	}
	
	internal MenuStrip menu;
	internal ToolStripMenuItem fileMenu;
	internal ToolStripMenuItem fileCreateNewMenu;
	internal ToolStripMenuItem fileOpenMenu;
	internal ToolStripMenuItem fileSaveAsMenu;
	internal ToolStripMenuItem fileSaveMenu;
	internal ToolStripMenuItem fileExitMenu;
	internal TextBox editArea;
}

一応、せっかくここまでやったので、

  • カーソルの位置(行と列)を表示する機能
  • ウィンドウ内へファイルをDnDすることで開く機能
  • オートインデント機能
  • アンドゥ&リドゥ機能
  • ワイルドカード対応の検索機能
  • 正規表現対応の置換機能
  • 単純なシンタックスハイライト機能

くらいはつけたいな、と思ってます。思ってるだけでやらない可能性の方がはるかに高いけど。