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することで開く機能
  • オートインデント機能
  • アンドゥ&リドゥ機能
  • ワイルドカード対応の検索機能
  • 正規表現対応の置換機能
  • 単純なシンタックスハイライト機能

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

Configクラスの実装サンプル

前回のエントリでちょっと載せたコードについて。
Configクラスを毎回書くのがだるかったので基底クラスを作ってそれを継承するようにしました。

この3つを基底クラスに定義したことで、Configクラスは非常にスッキリしたように思います。

本当はシングルトンパターンもSingleton<T>みたいなクラス作って継承させたかったんですが、Instanceプロパティの型の定義に四苦八苦して、結局派生クラス(この場合Configクラス)に直接書きました。

ちなみに、ConfigクラスにはINotifyPropertyChangedインターフェースは実装していません。
せっかくシンプルにまとめたクラスが見にくくなってしまうと思ったからです。
Notify Property Weaverという大変便利なVisual Studio用のアドオンもあるのですが、配布サイトが既になくなってしまっているのと、VS2010専用であるため、今回は考慮していません。

Configクラスの使い方

const string ConfigFileName = "hogefuga.xml";

string dataPath = mbApiInterface.Setting_GetPersistentStoragePath();
string configPath = Path.Combine(dataPath, ConfigFileName);

// 読み込み
// ※configPathにファイルが存在しない場合や、
//  XMLのデシリアライズに失敗すると、勝手に内部でLoadDefaultを呼びます。
Config.Instance.Load(configPath);

// プロパティ取り出し
Console.WriteLine(Config.Instance.ExampleString); // => example

// プロパティ書き込み
Config.Instance.ExampleBoolean = false;

// 保存
Config.Instance.Save(configPath);

MusicBeeのプラグイン開発

最近、MusicBeeというWindows向けのオーディオプレイヤーのプラグイン開発ばかりやってます。
本体がVer2.0辺りから.NET化したのもあって、プラグインも.NET(C#VB.NETC++/CLI)で非常に手軽に開発することができます。
コントロールはWinFormsです。
ビルドしたプラグインは主に2chのMusicBeeスレで公開していますが、
開発していくにあたっていろいろとノウハウも貯まってきたので、ここいらでいくつかご紹介します。
公式Wikiに書かれている内容もありますが、一応。

以下出てくるmbApiInterfaceという謎の変数ですが、
これはMusicBeeのAPIにアクセスするためのインターフェースのインスタンスです。
公式Wikiで配布されているプラグインソースコードをDLすると初めから宣言されています。

メインパネルで表示されている曲をすべて取得

public IEnumerable<string> GetDisplayedSongs()
{
    return GetSongs("domain=DisplayedFiles");
}

public IEnumerable<string> GetSongs(string query)
{
    if (mbApiInterface.Library_QueryFiles(query))
    {
        while (true)
        {
            string filepath = mbApiInterface.Library_QueryGetNextFile();
            if (string.IsNullOrEmpty(filepath)) yield break;
            yield return filepath;
        }
    }
}

ちなみに、ライブラリに存在する曲を丸ごとすべて、という場合には

public IEnumerable<string> GetAllSongs()
{
    return GetSongs("domain=Library");
}

とすることで可能です。

設定画面のコントロール作成をべた書きしなければいけない問題

MusicBeePlugin.PluginクラスのConfigureメソッドは、
内部変数aboutConfigurationPanelHeightプロパティに0以外を代入することによって、設定画面のPanelコントロールのハンドルをIntPtr構造体で受け取ります。
サンプルソースコードではこれに直接コーディングでコントロールを生やしていますが、
これをユーザーコントロールとして一まとめに作成したものを追加することで見通しがよくなります。
MusicBeeの設定画面で「保存」を押して閉じたときにMusicBeePlugin.PluginクラスのSaveSettingsメソッドが走るのですが、ここで編集した内容を反映させるため、私は設定を一まとめにプロパティで持ったConfigクラスを作り、そのインスタンスを作成したユーザーコントロールに参照渡ししています。

public bool Configure(IntPtr panelHandle)
{
    if (panelHandle != IntPtr.Zero)
    {
        Panel configPanel = (Panel)Control.FromHandle(panelHandle);
        var childUserControl = new MyUserControl(ref this.config);
        configPanel.Controls.Add(childUserControl);
    }
    return false;
}
追記(02/13 0:27)

Configクラスのインスタンスを参照渡しするより、シングルトンパターン使った方がスッキリしますね。

public class Config
{
    private Config() { }
    
    private static Config instance = new Config();
    public  static Config Instance { get { return Config.instance; } }
}

ファイルへのタグ書き込みが上手くいかない問題の解決

公式のフォーラムをよく読むと分かるのですが、MusicBeePlugin.Plugin.MusicBeeApiInterfaceクラスの中身を読んでいるだけでは気づけないんじゃないかと思います。

public void WriteTag(string filepath, MetaDataType type, string value)
{
    mbApiInterface.Library_SetFileTag(filepath, type, value);
    mbApiInterface.Library_CommitTagsToFile(filepath); // ここ大事
}

実はLibrary_SetFileTagメソッドはただバッファリングしているだけで、このメソッドを走らせているだけでは何重に実行しようとも実際に反映されることはありません。
反映させるためにはLibrary_CommitTagsToFileメソッドを走らせる必要があります。
ここで割とハマりました。


こんなところですか。
本当はもっとある気がするんだけど、すぐには浮かんでこないのと、文章にとりとめがなくなりすぎてて意味不明になってしまうので。

蛇足

デザイナで編集してると気づかないうちに余計なプロパティまで変更してること、ありますよね。

.NET Framework における System.String について

今年に入って初めて .NET Framework にまともに触れるようになってきたのでまだまだ勉強中です。
趣味で触るのはC#、仕事で触るのはVB.NETって感じです。

さて、だいたいどの解説サイトを見ても「C#VB.NETもできることは同じ」というような解釈になってます。
ただ、System.Stringが言語によって扱いが異なる気がしています。

あるとき、VB.NETNullable(Of String)と書いたときに静的エラーが出力されました。
妙だと思い調べてみると、System.Stringは参照型であるというのです。

基本的な型のうち int, double, byte, char などは値型ですが、
string は「値型のように振る舞う参照型」であるというような記述を見ました。

参照型であるということは null を代入できるのか。
と思い、以下のコードを書きました。

C#
public class Program
{
    public static void Main(string[] args)
    {
        string string1 = null;
        string string2 = "";
        string string3 = string.Empty;

        Console.WriteLine("1:{0}", string1 ?? "string1 is null");
        Console.WriteLine("2:{0}", string2 ?? "string2 is null");
        Console.WriteLine("3:{0}", string3 ?? "string3 is null");

        Console.WriteLine(string1 == string2); // null         == ""
        Console.WriteLine(string2 == string3); // ""           == string.Empty
        Console.WriteLine(string3 == string1); // string.Empty == null
    }
}
VB.NET
Public Module Program
    Public Sub Main(args() As string)
        Dim string1 As String = Nothing
        Dim string2 As String = ""
        Dim string3 As String = String.Empty
        
        Console.WriteLine("1:{0}", If(string1, "string1 is Nothing"))
        Console.WriteLine("2:{0}", If(string2, "string2 is Nothing"))
        Console.WriteLine("3:{0}", If(string3, "string3 is Nothing"))
        
        Console.WriteLine(string1 = string2) ' Nothing      = ""
        Console.WriteLine(string2 = string3) ' ""           = String.Empty
        Console.WriteLine(string3 = string1) ' String.Empty = Nothing
    End Sub
End Module

動作デモ*1

C#     : http://rextester.com/ELTMU58022
VB.NET : http://rextester.com/IRCW50181

と、ここまで来て二つの言語間で結果が違ったことに気づきました。

これ、 ""String.Emptyエイリアスの関係なので言うまでもなく同じものです。
ですから、比較したときにtrueになるのは当然だと思います。
でも、

string string1 = null;
string string2 = "";
Console.WriteLine(string1 == string2); // ==> false
Dim string1 As String = Nothing
Dim string2 As String = ""
Console.WriteLine(string1 = string2) ' ==> True

なのです。
VB.NETではデフォルトでOption Strict Offであるため暗黙の型変換が行われたのかと思い、Option Strict Onを記述するも変わらず。

ちなみに、

Dim string1 As String = Nothing
Dim string2 As String = ""
Console.WriteLine(string2.Equals(string1)) ' ==> False

でした。

この挙動について、このエントリを書いている途中まではSystem.Stringの特殊性によるものかと思っていましたが、書いている途中でそれぞれの言語の等価演算子の振る舞いの違いによるものかも?ともちょっと思っています。
どうなんですかね、これ。

何とも尻切れトンボ感あるエントリになってしまいましたが、この辺で。

追記 (2014/11/11 1:21)

どうやら、やはり等価演算子自体が違う振る舞いをするようです。

*1:rextester.comは鯖が落ちていることが稀によくある

Co-opゲーが好きで対戦ゲーがあまり好きでない理由

最初に

独り言なので読む価値ないですさようなら

本題

ゲームを仕事と見る向きもありますが*1
基本的にゲームはあくまでも暇つぶしの道具であり、遊びなので、
楽しめなければ意味がありません。

そして勝負要素のあるゲームというのは、どちらかが必ず負けるか引き分けに終わる。
たいていの人は負けると悔しい(ですよね?)ので、その苦境を楽しめるかどうかで対戦ゲーの好き好きが決まってくると思うんですが、私は苦手です。
特にFPS二人零和有限確定完全情報ゲームなど本人のスキルに多分に左右される勝ち負けは、相手に見合う実力がなければ一方的にただ蹂躙されるだけになってしまうため、遊びとは程遠い結果に終わることが多いです。
それを楽しめる人を私は見てみたい。

一方、Co-opでは他の人と一緒にゲームを進行させて楽しみを共有できます。
共通の目的があるので一緒に達成できます。あるいは一緒に失敗できます。
失敗してもみんなと一緒にワイワイできれば楽しいんです。

そういう意味では、対戦ゲーでも多人数vs多人数なら同じチームのメンバーによっては楽しく遊べます。
勝てたらもちろん嬉しいですし、負けても楽しめます。
ただし、例えば野良で入ったチームに異常に上手い人が一人いて、その人がワンマンプレイでチームを勝利に導く場合はその限りではありません。
「勝たされている」感じがしてしまう。
せっかくチームを組むのだから協力して、その先に勝つためのきっかけを作るという、そこまでの過程に楽しみを見出すタイプです。私は。
他の人との行動が綺麗に噛み合い、その結果ものすごい成果を生み出すときに、言い知れないカタルシスを覚えます。

ちょっと話が対戦ゲー寄りの中身になりましたが、今書いたことはCo-opにも言えると思います。
協力から結果を生み出すのが好きなんです。
そういうところ、誰にだってあると思うんですが、どうでしょうか。

*1:現にプロゲーマーという職業がありますよね。日本人だと梅原大吾氏。