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.NET、C++/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
メソッドは、
内部変数about
のConfigurationPanelHeight
プロパティに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
メソッドを走らせる必要があります。
ここで割とハマりました。
こんなところですか。
本当はもっとある気がするんだけど、すぐには浮かんでこないのと、文章にとりとめがなくなりすぎてて意味不明になってしまうので。
蛇足
デザイナで編集してると気づかないうちに余計なプロパティまで変更してること、ありますよね。
DataGridViewのReadOnlyプロパティがいつの間にかtrueになっていたことに気づかず、新規行追加できねぇ!なんでだ!!って2日くらいハマってた。
— ところてん (@htsign) February 10, 2015
.NET Framework における System.String について
今年に入って初めて .NET Framework にまともに触れるようになってきたのでまだまだ勉強中です。
趣味で触るのはC#、仕事で触るのはVB.NETって感じです。
さて、だいたいどの解説サイトを見ても「C#もVB.NETもできることは同じ」というような解釈になってます。
ただ、System.String
が言語によって扱いが異なる気がしています。
あるとき、VB.NETでNullable(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)
どうやら、やはり等価演算子自体が違う振る舞いをするようです。
@htsign ちょい前のやつ、気になってil覗いてみたけど、演算子の実体が違うみたいやで。
— michi (@michi1129) 2014, 11月 10
*1:rextester.comは鯖が落ちていることが稀によくある
Co-opゲーが好きで対戦ゲーがあまり好きでない理由
最初に
独り言なので読む価値ないですさようなら
本題
ゲームを仕事と見る向きもありますが*1、
基本的にゲームはあくまでも暇つぶしの道具であり、遊びなので、
楽しめなければ意味がありません。
そして勝負要素のあるゲームというのは、どちらかが必ず負けるか引き分けに終わる。
たいていの人は負けると悔しい(ですよね?)ので、その苦境を楽しめるかどうかで対戦ゲーの好き好きが決まってくると思うんですが、私は苦手です。
特にFPSや二人零和有限確定完全情報ゲームなど本人のスキルに多分に左右される勝ち負けは、相手に見合う実力がなければ一方的にただ蹂躙されるだけになってしまうため、遊びとは程遠い結果に終わることが多いです。
それを楽しめる人を私は見てみたい。
一方、Co-opでは他の人と一緒にゲームを進行させて楽しみを共有できます。
共通の目的があるので一緒に達成できます。あるいは一緒に失敗できます。
失敗してもみんなと一緒にワイワイできれば楽しいんです。
そういう意味では、対戦ゲーでも多人数vs多人数なら同じチームのメンバーによっては楽しく遊べます。
勝てたらもちろん嬉しいですし、負けても楽しめます。
ただし、例えば野良で入ったチームに異常に上手い人が一人いて、その人がワンマンプレイでチームを勝利に導く場合はその限りではありません。
「勝たされている」感じがしてしまう。
せっかくチームを組むのだから協力して、その先に勝つためのきっかけを作るという、そこまでの過程に楽しみを見出すタイプです。私は。
他の人との行動が綺麗に噛み合い、その結果ものすごい成果を生み出すときに、言い知れないカタルシスを覚えます。
ちょっと話が対戦ゲー寄りの中身になりましたが、今書いたことはCo-opにも言えると思います。
協力から結果を生み出すのが好きなんです。
そういうところ、誰にだってあると思うんですが、どうでしょうか。