ニコニコC++入門

入門サイトや市販の入門書では絶対に教えてくれない、C++の本当の使い方を教えます。

別スレッドからフォームのコンポーネントを操作するには

2005-07-31 23:24:41 | Managed C++

.net Frameworkのウィンドウコンポーネントはスレッドセーフではありません。ウィンドウのコンポーネントはメインスレッドで処理されますから、自分で作成したスレッドからフォーム上のコンポーネントのプロパティを操作する場合はInvokeしなければならないのが.netの作法らしいです。Invokeしなくてもなんとなく動作しますが、原因不明のデッドロックに悩まされることになるでしょう。

__delegate void   delegateSetMode(System::Int32 mode);

 delegateSetMode *p = new delegateSetMode(panel, &ControlPanel::SetMode);
 p->BeginInvoke(mode, 0, 0);

InvokeするとSetModeの呼び出しが予約されます。この時点では実際にはメソッドコールは行われません。呼び出されたスレッドが適当なタイミングでSetModeを呼び出します。

Invoke呼び出しには2種類あります。

Invoke
 相手スレッドがそれを処理し終えるまでブロックする。

BeginInvoke
 相手スレッドに処理を委託するだけでブロックしない。

 BeginInvokeで依頼した処理が終了したかどうかを判定する方法は2つあります。
 EndInvokeを呼び出す。→処理が終わるまでブロックされます。
 BeginInvokeの引数にコールバック関数を設定する。→処理完了時に設定したコールバック関数が呼ばれます。

__delegateというキーワードですが、内部的にはMulticastDelegateというクラスを継承して新しいクラスを生成するようです。つまり上記のdelegateSetFrameというのはMulticastDelegateから派生した1つのクラスであるわけです。__delegateは関数内で宣言できない、使うときにnewしなければならないなどの不思議なルールや制限がありますが、クラスの宣言だということがわかればすんなり理解できるでしょう。


デバッグメッセージを出力するには?

2005-07-31 23:18:53 | Managed C++

いわゆるprintfデバッグと呼ばれるものがあります。しかしSystem::ConsoleやprintfはWindowsアプリの場合は出力されません。

そこで代わりにSystem::Diagnostics::Debug::WriteLineを使います。

System::Diagnostics::Debug::WriteLine(String::Format(S"A>{0}", GetFrame().ToString()));

これでVisualStudioの「出力」ウィンドウにメッセージを出力させることができます。このDebugクラスには他にも便利な機能がありそうです。


数値を16進数表記の文字列に変換するには?

2005-07-31 23:17:20 | Managed C++
10進表記の場合はvalue.ToString()でいけるわけですが、16進表記の場合はvalue.ToStrint("X")とします。

メニューにショートカットを割り振るには?

2005-07-31 23:16:39 | Managed C++

MenuItemのShortcutプロパティにショートカットを設定します。終わり。

…といきたいところですが、選べるキーの数が非常に少ないので使い物になりません。

しかし手動でハンドルすればあらゆるキーの組み合わせに対応できます。

まずはキー入力イベントハンドラを作ります。

System::Void Form1::Form1_KeyDown(System::Object * sender, System::Windows::Forms::KeyEventArgs * e)
{
 using namespace System::Windows::Forms;
 switch(e->KeyCode)
 {
 case Keys::Tab:
  if (!e->Shift)
   menuItemNext_Click(sender, e);
  else
   menuItemPrev_Click(sender, e);
  break;
 }
}

次に、MenuItemのTextプロパティを修正して、ショートカットキーを表示します。

Windows APIには、メニュー項目のテキストに\tがあるとそこから右がショートカットであることが
意味しているように右に寄せてくれるという便利な機能があります。

ところがデザイナでTextプロパティに"&Next\tTab"という文字列を設定しても\tがそのまま表示されてしまいます。

そこで毎度お馴染みのInitializeComponentメソッドでTextを設定している部分を直接書き換えてやると、デザイナのTextプロパティにも反映されます。


プログラムの多重起動を防止して、起動済みプログラムにパラメーターを転送するには?

2005-07-31 23:03:58 | Managed C++

WM_COPYDATAメッセージを送ります。

using namespace System::Runtime::Serialization::Formatters::Binary;
using namespace System::IO;
using namespace System::Runtime::InteropServices;
using namespace System::Diagnostics;

// 実行中の同じアプリケーションのプロセスを取得する
System::Diagnostics::Process *GetPreviousProcess()
{
 Process *curProcess = Process::GetCurrentProcess();
 Process *allProcesses[] = Process::GetProcessesByName (curProcess->ProcessName);

 for (int i = 0; i < allProcesses->Length; i++)
 {
  Process *checkProcess = allProcesses[i];
  // 自分自身のプロセスIDは無視する
  if (checkProcess->Id != curProcess->Id)
  {
   // プロセスのフルパス名を比較して同じアプリケーションか検証
   if (String::Compare(checkProcess->MainModule->ModuleName, curProcess->MainModule->ModuleName, true) == 0)
   {
    // 同じフルパス名のプロセスを取得
    return checkProcess;
   }
  }
 }

 // 同じアプリケーションのプロセスが見つからない!
 return 0;
}

//Stringの配列をWM_COPYDATAで送信する関数
void SendData(System::IntPtr hdl, String *str[])
{
 BinaryFormatter *bf = new BinaryFormatter;
 MemoryStream *ms = new MemoryStream;
 bf->Serialize(ms, str);
 size_t size = static_cast<size_t>(ms->Length);
 char *data = new char[size];
 Marshal::Copy(ms->GetBuffer(), 0, data, size);

 COPYDATASTRUCT ds;
 ds.dwData = WM_COPYDATA;
 ds.cbData = size;
 ds.lpData = data;
 SendMessage((HWND)hdl.ToPointer(), WM_COPYDATA, (WPARAM)0, (LPARAM)&ds);

 delete []data;
}

//オーバーライドしたフォームのWndProc関数でWM_COPYDATAを受信
void Form1::WndProc(System::Windows::Forms::Message *msg)
{
 if (msg->Msg == WM_COPYDATA)
 {
  COPYDATASTRUCT *ds = reinterpret_cast<COPYDATASTRUCT *>(msg->LParam.ToPointer());

  BinaryFormatter *bf = new BinaryFormatter;
  MemoryStream *ms = new MemoryStream;
  unsigned char data __gc[] = new unsigned char __gc[ds->cbData];
  Marshal::Copy(ds->lpData, data, 0, ds->cbData);
  ms->Write(data, 0, ds->cbData);
  ms->Seek(0, System::IO::SeekOrigin::Begin);
  String *files[] = __try_cast<String * __gc[]>(bf->Deserialize(ms));
  ReadFromFiles(files);
  return;
 }
 __super::WndProc(msg);
}
 
int APIENTRY _tWinMain(HINSTANCE hInstance,
 HINSTANCE hPrevInstance,
 LPTSTR lpCmdLine,
 int nCmdShow)
{
 System::Threading::Thread::CurrentThread->ApartmentState = System::Threading::ApartmentState::STA;

 //起動済みプロセスのチェック
 System::Diagnostics::Process *prev = GetPreviousProcess();
 if (prev)
 {
  SendData(prev->MainWindowHandle, Environment::GetCommandLineArgs());
  return 0;
 }

 Application::Run(new Form1());
 return 0;
}

lpCmdLineをWM_COPYDATAで送信すればもっと簡単なのではないかと思うかもしれません。

しかしコマンドラインに渡されるファイル名はロングファイル名なので、スペースを単純にコマンドの区切りとして解釈できません。

せっかくEnvironment::GetCommandLineArgsで文字列の配列として取得できるのですから、これをそのまま送信できれば、"の範囲がどうとか面倒なことは考えなくても済むわけです。

そこでシリアライザを利用してStringの配列オブジェクトをシリアル化しています。シリアライザには、XML、SOAP、バイナリの3種類があり、ここではバイナリのシリアライザを使用しています。

出力先となるストリームはMemoryStreamです。しかしここでまた問題が。MemoryStreamに出力されたByteの配列のポインタは、CLIで管理されている仮想的なポインタなので、このままではプロセスを超えられません。そこでマーシャリングを使用して、アンマネージドな配列にコピーしています。

マーシャリングというのはこのような非CLI環境とのデータ交換のことらしいです。Marshalクラスにはそのための謎な機能がたくさんあります。

送信したメッセージはFormのWndProcをオーバーライドして受信します。ゲットしたデータをStringの配列に戻すためにデシリアライズしています。


アセンブリ情報を読み込むには

2005-07-31 23:00:46 | Managed C++

.netの世界では、アセンブリというのは要するに実行ファイルのことです。実行ファイルの情報を設定するには、AssemblyInfo.cppを適当にいじります。

そしてその情報を取得するには以下のようにします。

System::Reflection::AssemblyName *name = System::Reflection::Assembly::GetExecutingAssembly()->GetName();

labelName->Text = name->Name;
labelVersion->Text = String::Format(S"hoge Version {0}", name->Version); 

バージョン情報以外の情報はこんな感じで読みます。

template<class T> T *GetAttrib(Assembly *myAsm)
{
 return dynamic_cast<T *>(myAsm->GetCustomAttributes(__typeof(T), true)->Item[0]);
}

System::Void AbortDialog::AbortDialog_Load(System::Object * sender, System::EventArgs * e)
{
 Assembly *myAsm = Assembly::GetExecutingAssembly();

 labelName->Text = GetAttrib<AssemblyProductAttribute>(myAsm)->Product;
 labelVersion->Text = String::Format(S"{0} Version {1}", labelName->Text, myAsm->GetName()->Version);
 labelCopyRight->Text = GetAttrib<AssemblyCopyrightAttribute>(myAsm)->Copyright;
}


アプリのアイコンを登録するには?

2005-07-31 22:58:19 | Managed C++

C#だとプロジェクトのプロパティから設定できますが、MC++ではどうすればよいのでしょうか。

よくわからないのですがとにかく出来たのでまとめておきます。

1. icoファイルを作る。

VisualStudioのリソースエディタに作成したBMPを読ませると激しく減色するので、aiconなどのアイコン作成ツールで作ります。

2. app.icoを新しいiconで上書きしてapp.rcをビルドする。

他にも方法がありそうなものですが…。

3. 各フォームのIconプロパティにアイコンを設定する。

もしかしたら 2. をやれば自動的にフォームのアイコンも変わるのかも? めんどくさいのでそこまで検証していません。

4. リリース版をビルドする。

デバッグ版をいくらビルドしてもなぜかアプリのアイコンは変わらないようです。


フォームのウィンドウハンドルを得るには?

2005-07-31 22:53:54 | Managed C++

フォームのハンドルはHandleプロパティで得られますが、このハンドルをWindows APIに渡すには次のようにします。

hWnd = reinterpret_cast<HWND>(Handle.ToPointer());


オブジェクトをXMLに入出力するには?

2005-07-31 22:50:45 | Managed C++

DataSetにはWriteXmlとかいうメソッドがあるわけですが、オブジェクトやコンテナにはありません。もしかしてXmlDocumentとかを自分で使うしかないのでしょうか?

いやいや、XmlSerializerを使えば、オブジェクトや配列をXMLに入出力できます。

http://dobon.net/vb/dotnet/file/xmlserializer.html
http://dobon.net/vb/dotnet/file/xmlserializer2.html

C++で実装する場合の注意は…。

  • XMLに出力できるオブジェクトは__gcクラスのインスタンスだけ。
  • デフォルトコンストラクタ(引数のないコンストラクタ)が必要。

TreeViewをドラッグ&ドロップで付け替えるには

2005-07-31 22:46:14 | Managed C++

これくらい標準で対応してくれてもよさそうですが、自分でイベントハンドラを書かないといけません。ヘルプにサンプルがありますが、子ノードにドラッグすると丸ごと消えるバグ付です。

しかしネットで検索して出てくるサンプルは逆に高機能すぎで使えません。そこで自分の書いたサンプルを公開しておきます。

void treeView1_ItemDrag(System::Object * sender, System::Windows::Forms::ItemDragEventArgs * e)
{
 TreeNode *srcNode = dynamic_cast<TreeNode *>( e->Item );
 if (!srcNode)
  return;
 if (srcNode->Parent == 0)
 {
  //ルートは移動不可能
  return;
 }
 DoDragDrop(e->Item, DragDropEffects::Move);
}

void Form1::treeView1_DragEnter(System::Object * sender, System::Windows::Forms::DragEventArgs * e)
{
 e->Effect = DragDropEffects::Move;
}

void Form1::treeView1_DragDrop(System::Object * sender, System::Windows::Forms::DragEventArgs * e)
{
 if ( e->Data->GetDataPresent(__typeof(TreeNode)) )
 {
  //クライアント座標を取得
  Point pt = dynamic_cast<TreeView *>(sender)->PointToClient(System::Drawing::Point(e->X, e->Y));
  //対象のノードを取得
  TreeNode *destNode = dynamic_cast<TreeView *>(sender)->GetNodeAt(pt);
  //ドラッグ元のノードを取得
  TreeNode *srcNode = dynamic_cast<TreeNode *>( e->Data->GetData(__typeof(TreeNode)) );
  //同じツリービューなら移動
  if(destNode->TreeView == srcNode->TreeView)
  {
   if (!IsChildNode(srcNode, destNode)) //子供には移動できない
   {
    destNode->Nodes->Add( dynamic_cast<TreeNode *>(srcNode->Clone()) );
    destNode->Expand();
    //Remove Original Node
    srcNode->Remove();
   }
  }
 }
}

bool Form1::IsChildNode(TreeNode *parent, TreeNode *child)
{
 if (child->Parent == parent)
  return true;
 else if (child->Parent != 0)
  return IsChildNode(parent, child->Parent);
 else
  return false;
}