Programming」カテゴリーアーカイブ

プログラミングの話題

DataGridViewをActiveXコントロールにラップしてTcl/Tkのウィンドウに埋め込む(1)

先日Tcl/TkのGUIにデータグリッドを、ということでTkTableをいじっていましたが、やっぱりいろいろ大変だということで、.NET FrameworkのDataGridViewをTkのウインドウに埋め込む実験をしてみました。これならほとんどデフォルトのバインディングでも文句はないでしょう。
 
さて、C#で作ったユーザーコントロールをActiveXコントロールにする方法は比較的簡単です。プロジェクトのプロパティでそれっぽいところに2箇所ほどチェックを入れてビルドすればCOM参照可能になります。
 
ビルドしてできたDLLはP/Invoke的な方法で利用することはできないです。マネージド環境の外から使うため、COMに登録する必要があります。
 
手動でインストールする場合は、WindowsSDKに含まれるregasm.exeを使って、
regasm SimpleDgv.dll /tlb:SimpleDgv.tlb /codebase
 
アンインストールは
regasm SimpleDgv.dll /unregister
 
COMへの登録に成功したら、次はいかにTclから利用するかです。ActiveTclには標準でtcomというCOMを利用するためのパッケージが付いてきますが、ActiveXコントロールをTkに埋め込むという目的には使えません。代わりにOptclというパッケージが存在します。これはActiveTclに含まれていないため、Tcler's Wikiにあったリンクからコンパイル済みのバイナリをとってきました。
 
System.Windows.Forms.dllがそのままCOMにできればいいなと一瞬思ったんですが、そうしたところでOptclで使うのが難しい引数や型もあるので、あまり意味がない気がします。必要そうなものに限ってラッパーメソッドを作って公開することにします。
 
以下の図は、上がC#のフォームにユーザーコントロールを貼り付けたもの(これはCOMではありません)。下の方が、optclを使ってActiveXコントロールとしてTkのウインドウに貼り付けたものです。
 
 
日本語が文字化けしていますね。どうしてこうなるのか、よく分かっていません。
文字列はソースファイルに埋め込んであります。
 
TclのソースはShiftJISで、C#はUTF-8 BOM有りとなっています。
聞くところによると、Tclは内部エンコーディングとしてUTF-8を、C#はunicodeを採用しているそうですが、その辺の問題でしょうか?
optclが出す(C#のエラーメッセージと思われる)も文字化けしてて復元できないんですよね。
 
いろいろといじってみてはいますが、解決していません。
 
encoding convertto unicode
encoding convertto cp932
 
とかは意味なかったです。このコマンドもあまり理解してないんですけどね。
 
C#でバイト列から文字コードを判定する方法はあちこちで紹介されていたので、Tclから渡すときに文字列をbinary formatしてbyte[]で渡そうと思ったらoptclの制限で配列引数は未実装とのこと。
 
うーん困った。
 

2009/9/2 -- 追記: とりあえずC#の側で解決しました。

byte[] b = Encoding.Default.GetBytes(s);
string u = Encoding.UTF8.GetString(b);

要するに、TclはUTF-8をデコードして送ってるのに、C#はシステムのデフォルトエンコーディング(Shift_JIS)としてエンコードしてるようなのです。

だからその逆をたどってやればよいわけで、まず文字化けした文字列をShift_JISとしてバイト列に戻します。これをUTF-8エンコードしてやることで、本来の文字列に戻してやることができるという理屈です。ただし、これはもともとUTF-8で送ってくるクライアントにしか対応できません。2行目が決めうちだからです。


2009/9/2 -- 追記: やっぱだめ

うまくできたと思ったのはひらがなだけで、漢字やカタカナは一部が文字化けしてしまいました(例: 選択 -> 選・)。

ここによれば、一度間違って変換されたものは可逆性を失うようです。
http://dobon.net/vb/dotnet/string/getencoding.html

最初から不可逆なんだからC#で直すことはできないってことになる。

あとはTclからそもそもShift_JISで送れるようにするか、base64でASCIIとして送って、デコードするかしかなさそう。


2009/9/2 -- 追記: 成功

やっぱりbase64でやることにした。いちいち変換しないといけなくてめんどいけど、しょうがあるまい。

Tcl側のポイントは、マルチバイト文字を含む場合は一旦バイナリに変換しないといけないこと。encoding converttoを使う。
C#側はShift_JISのASCII部分だけ使うので、情報の損失がなくなった。返すときは任意のエンコーディングでかまわないようだが、Tcl側で対称性を持たせるためにシステムデフォルト(Shift_JIS)で返すようにした。

Tcl側のコード(エンコーディング省略でシステムデフォルト)

proc dec {s} {
 ::base64::encode [encoding convertto $s]; # Tcl -> C#
}

proc enc {s} {
 encoding convertfrom [base64::decode $s]; # C# -> Tcl
}

C#側のコード

namespace Extension
{
    public static class StringMethod
    {
        public static string Dec(this string s, Encoding encoding) // Tcl -> C#
        {
            var b = Convert.FromBase64String(s);
            b = Encoding.Convert(Encoding.Default, encoding, b);
            return encoding.GetString(b);
        }

        public static string Enc(this string s) // C# -> Tcl
        {
            var b = Encoding.Default.GetBytes(s);
            return Convert.ToBase64String(b);
        }
    }
}


Visual Studio 2008 C#ソリューションとTclのソースを固めて置いときます。ご自由に拾ってください。
 
 
 

今後の予定:
  1. イベントの定義(ボタンクリックとか)
  2. クリップボード操作(CSV/TSVプレーンテキスト)
  3. その他もろもろ

TkTableのバインディングを修正した

TkTableはdllで配布されていますが、ヘルプに説明のあるデフォルトのバインディングはlib/Tktable2.9/tkTable.tclに書かれているようです。
このファイルは特にデモアプリなどで使われているわけではなく、単にカスタマイズする人のために内部の実装を見えるようにしたものだと思われます。

TkTableはオプションで-selectmode extendedを指定することでドラッグによる範囲選択ができるようになるのですが、セル領域とタイトルセル領域にまたがってドラッグした場合、選択領域がばらばらになってしまうという問題がありました。

最新の2.10に添付されたtkTable.tclも全く変わってなかったので、修正してみました。ただ、過去の選択部分だけをクリアするような処理を、全域クリアに変えてしまったので、パフォーマンス的には問題ありの可能性もあります。ためしに3000行×50列とかでやってみたところでは全く問題ありませんでした。
 

--- C:/Tcl/lib/Tktable2.9/tkTable.tcl	Tue Aug 25 20:01:59 2009
+++ C:/devel/tcl/tktable/tkTable.tcl	Fri Aug 28 18:59:51 2009
@@ -433,26 +433,54 @@
 	    if {[catch {$w index anchor}]} { return }
 	    scan $Priv(tablePrev) %d,%d r c
 	    scan $el %d,%d elr elc
-	    if {[$w tag includes title $el]} {
-		if {$r < [$w cget -titlerows]+[$w cget -roworigin]} {
-		    ## We're in a column header
-		    if {$c < [$w cget -titlecols]+[$w cget -colorigin]} {
-			## We're in the topleft title area
-			$w selection clear anchor end
-		    } else {
-			$w selection clear anchor [$w index end row],$c
-		    }
-		    $w selection set anchor [$w index end row],$elc
-		} else {
-		    ## We're in a row header
-		    $w selection clear anchor $r,[$w index end col]
-		    $w selection set anchor $elr,[$w index end col]
+
+	    set titlerowend [expr {[$w cget -titlerows]+[$w cget -roworigin]}]
+		set titlecolend [expr {[$w cget -titlecols]+[$w cget -colorigin]}]
+	    if {$r != $elr} {
+			if {$r < $titlerowend} {
+				if {$elr >= $titlerowend && $r >= $titlerowend} {
+					set elr $r; # turn aside while selecting title row area
+				}
+			} else {
+				if {$elr < $titlerowend && $r < $titlerowend && $elc >= $titlecolend} {
+					set elr $r; # turn aside while selecting non-title row area
+				}
+			}
 		}
+		if {$c != $elc} {
+		    if {$c < $titlecolend} {
+				if {$elc >= $titlecolend && $c >= $titlecolend} {
+					set elc $c; # turn aside while selecting title col area
+				}
+			} else {
+				if {$elc < $titlecolend && $c < $titlecolend && $elr >= $titlerowend} {
+					set elc $c; # turn aside while selecting non-title col area
+				}
+			}
+		}
+		$w selection clear all
+	    if {[$w tag includes title $r,$c]} {
+			if {$r < $titlerowend} {
+			    if {$c < $titlecolend} {
+					## We're in the topleft title area
+					if {$elc >= $titlecolend} {
+					    $w selection set anchor [$w index end row],$elc
+					} else {
+					    $w selection set anchor $elr,[$w index end col]
+					}
+			    } else {
+				    ## We're in a column header
+				    $w selection set anchor [$w index end row],$elc
+			    }
+			} else {
+			    ## We're in a row header
+			    $w selection set anchor $elr,[$w index end col]
+			}
+		    set Priv(tablePrev) $elr,$elc
 	    } else {
-		$w selection clear anchor $Priv(tablePrev)
-		$w selection set anchor $el
+			$w selection set anchor $el
+		    set Priv(tablePrev) $r,$c
 	    }
-	    set Priv(tablePrev) $el
 	}
     }
 }

Tcl/TkからC#へ

大学でC++に挫折して、プログラマはあきらめていた私はサークルのホームページでPerl/CGIを使ったのをきっかけに再びプログラミングに手を染めることになりました。

それでも仕事で使えるレベルではないだろうと、会社に入った時点ではプログラマになることは断っていたのですが、会社のCRM立ち上げに携わった後に部署がなくなり、開発部にまわされました。

最初にやったのがカメラのPTZ制御プログラムにPELCO-Dプロトコルを追加するというC++の小さな案件でした。それはそれっきりで、次にやったのがTcl/Tkのタッチパネル式のGUIアプリケーションをベースにした開発案件でした。それ以来、3年ほどWindowsアプリケーションをTcl/Tkで作ることになりました。

Tclの自由度は高く、XOTclなどでオブジェクト指向言語のようにも使えます。C#のクラスはプロパティやメソッド以外にイベントというメンバを持つことができるのですが、これだってTclからすればさして特別な機能ではありません(コンテキストのキャプチャは確かに難しいですが)。Ruby on RailsのActiveRecordのようにスキーマを自動的にマッピングする機能はType-safeな方法では実現できないので、C#には同様のライブラリは存在しません(全てを文字列として扱えばよいだけなので、不可能というわけではないです。誰もやらないというだけで)が、Tcl/Tkでコンセプトを真似た簡易版を作って使っています。

しかし、Tcl/Tkの言語本体はそこそこのスピードで進化していくものの、ライブラリ群は放置されてるものが多いです。また、2002年くらいを境に日本語の書籍の発行は途絶えています。オブジェクト指向拡張はあるものの、Rubyみたいに自由度の高いXOTclにはActiveTclのIDEやデバッガが対応していません。

Tclは世間のうわさに反して優れた言語だと思うし、そこそこライブラリも充実してはいるのですが、やはり完璧なアプリを作るに当たって、いろいろと問題があるのは否めません。

BLT Toolkitはすばらしい拡張ライブラリですが、Tcl/Tkの最新版に対応していません。stubに対応すべくGeorgeさんががんばって更新しているようですが、依然完成のめどが立たない状況が続いてます。

ほかにも結構重要なライブラリがちょっとしたバグを残したまま放置された状態です(TclUDP, Tktableとか) 。

teapot化されたライブラリをEXEにラップするにはTclAppをバージョンアップしないといけないし。

新規開発に関しては既にTcl/Tkをあきらめて、C#を採用しています。Tclの自由度に比べれば堅苦しさは否めませんが、.NET Frameworkなら高品質なGUIが開発できるので、安心してお客さんに見せられます。

Tcl/Tkには印刷ダイアログを表示する機能がありません。DataGridView並みに高度なGUIがありません。TkTableやTablelistがあるじゃないか、というかもしれませんが、これらはDataGridViewに比べて非常にプリミティブです。

大学の研究室や社内で使うならまだしも、簡単にGUIを作れるというのは、現代においてもはや売り文句にならない気がします。

C#などのGUIを生かしつつTclの既存アプリを再利用することはできないのでしょうか?これについてはいろいろと調べてきましたが、なかなか有効な手段は見つかりません。WindowsであればCOMの仕組みを使うことで何とかいけるみたいな話(optcl)はありますが、果たして実際にやった人はいるのでしょうか?

エクスポート関数はともかく、引数の型が構造体だったりして、構造体のメンバがさらに構造体だったりしたら、どうするの?とか。

C++の方が相性はいいだろう。MFC DataGridというのが使えたらいいけど、クラスのインポートってできるの?とか。

TclBridgeというのは?高いなあ。ちゃんとメンテされてるの?

Eagleは?Tclの再実装?うーんそういうことじゃないんだよなあ。

じゃあ何ができたらいいのでしょう?

ネイティブのTclに.NETのクラスライブラリをインポートして、publicなクラスや構造体を自由に操作したい。これだ!

私にCの知識と遊んでいても給料払ってくれる社長がいたら多少貢献できると思うのだけど。

・・・いや、無理。私では力不足です。

こうしてネットのところどころでこういうボヤキを書いているのは誰かが作ってくれるかもとちょっと期待してるからなんですが、まあ、そんな都合よくはいかないよな。。。日本語だから外人読めないし。

LINQ to Entities + SQLiteでCAST(expression AS type)

前回のエントリにて、アルファベットと数字の混在する部屋番号というのを扱ったけれど、結局、文字部分を削除しても問題ないということになったので、それはまあ、良くなった。実はいろいろ調べてAlphanumeric sortというのがあると知ったのだが、まあ、いろいろ役にたつことだろう。

さて、それならわざわざ文字列にしなくてもよいだろうということで、INTEGERに変更した。それでずいぶん無駄なロジックやキャストなんかが減って、すっきりした。

しかし、そこに新たな要求が来た。

  1. 部屋番号の範囲を指定して、複数の居住者を検索する。
  2. 部屋番号を文字列とみなして前方一致検索する。

1. の場合、INTEGERなので問題ない。

2. の場合、WHERE句で文字列にキャストしないとLIKEが使えない。クライアントサイドでの絞込みは馬鹿らしい。

正直こんなもん、SQLiteなら文字列も数値も同じだから、LIKEが使えていいんじゃないのかと思うけど、LINQ to Entitiesではそうもいかない。ObjectQuery<T>のWhereは条件に文字列とプレースホルダが使えるから、その線で探してみた。CASTを使えばよいらしい。Googleで調べるのに2時間もかかったよ。はー疲れた。

ObjectQuery<User> userQuery = entities.User.Include("Room")
  .Where("CAST(it.Room.room_no AS System.String) LIKE @keyRoom",
  new ObjectParameter("keyRoom", keyRoom + "%"));

CAST (expr AS type) というのがSQLiteにもあって、それは結構すぐ見つかったけど、typeのところがC#の型になるとは思い浮かばず、TEXTか?VARCHAR(4)とかか?と意味不明なエラーに苦しんでいました。System.Data.SQLiteのせいじゃないかと思って、dotConnect for SQLiteを自腹で買おうか真剣に悩んで見つけたサンプルをCASTでgrepしたら見つかりました。

Download dotConnect for SQLite

デリゲートって型推論あるの?

例えばボタンクリックにイベントハンドラを追加するには

btnHello.Click += new EventHandler(btnHello_Click);

などとやるわけだけど、いつの間にか無意識に、こんな風にやってしまっていることに気づいた。

btnHello.Click += btnHello_Click;

あれ、まずいかな?と思ったけど、なんら支障なく動いているように見える。

これってどういう仕組み?型推論?

調べても匿名デリゲートやラムダ式のことは出てくるけど暗黙の型付けについては余り出てこないので結局よく分からない。

これで正しければこの上なく楽なのだが。

 

追記:

分かったわけではないけど、普通みたい。

eventキーワードで宣言した場合に限り、こういうシンタックスシュガーみたいなことができるのだと、今のところは解釈している。

SQLite3にDateTime.Ticksでタイムスタンプを保存したら

ぱっと見ただけじゃ時刻が分からない。ものすごい数字に強い人ならともかくも。

しかし、多分integer(C#ではlong)で保存した方がインデックスが有効になるし、速いと思うので、Ticksで保存したいと思う。

そんな場合は、こうすればよい。

select datetime(created_at/pow(10,7) - 62135888400, 'unixepoch', 'localtime') as d, * from events;

長い。unixepochに変換するためにこうなってしまう。もっと直接的に計算できる項を見つければいいんだろうけど。SQLiteではCREATE FUNCTIONできないようだし。

Visual Studio 2008のエディションごとの機能比較

Microsoft Visual Studio 2008 製品ラインの概要

よく知らないうちはStandardとProfessionalの違いがよく分からなかったんだけど、いろいろな機能を使えるようになってくると、無いと困るものも出てくる。次期バージョンを買う際には参考にしたい。

これから使う予定の機能が多いので、使って公開するかもしれないけれど。

  • Crystal Reports(いるでしょ?)
  • 単体テスト(うーん。使いたいけど。当分使えないかも。)
  • Officeアプリケーション開発(いる?かもなあ。。。)
  • モバイルデバイスサポート(そんのに手出してる余裕ないわ。)
  • SQLデバッグ(そんな機能あったっけ?あるとしてもSQL Serverしか対応してないよね。)

FTPサーバへのログインが遅い

C#でNAS(LinkStation)のFTPにアクセスしまくるソフトを作るのにTKFP.DLLを使ってみることにした。接続が切れたのを検知できないので、毎回ログインする方法を取ったのだけど、ログインするごとに10秒間待たされる。毎回一定時間待たされるので、多分何らかのタイムアウトが発生しているのだろうと考えた。

  1. IPアドレスで指定するとDNS逆引きが発生してタイムアウト。: バグ。ありえないことじゃない。[FIX] WebRequest クラスを使用して Web 要求を行う際の接続の問題
  2. LinkStationはProFTPdを使っていて、こいつのオプションUseReverseDNS off, IdentLookups offが設定されていないのかもしれない。: 不明。分かったところでどうしようもない。多分ログに名前を記録するためにデフォルトではONになっていると思われる。
  3. WindowsXPのファイアウォールのせい。: よくあること。なんかよく分からないネットワークがらみのことはこれを無効にすればたいてい解決。

Wiresharkでディスプレイフィルタ「tcp.port eq 21 or ftp」かけて見ていると、「220 server ready」が返って来るのが10秒かかっていた。「ftp 220 server ready slow」でぐぐってみると、てがかりとなる議論が。

ftp login very slow [Archive] - HowtoForge Forums | HowtoForge - Linux Howtos and Tutorials

どうやらファイアウォールに引っかかっているためのようだ。ためしにファイアウォールを無効にすると待たされなくなった。しかし、無効にしとくわけにもいかないので、開くポートを限定したい。Wiresharkのフィルタを外してよく見てみると、ident 113ポートへのアクセスが3回記録されていた。どうやらこれのようだ。

「ftp 113」でぐぐってみると答えにたどり着いた。

LunaTear: XP SP2でFTPが遅くなる

参考:

DisplayFilters - The Wireshark Wiki

Windows Serviceアプリケーションの開発

最初にC#に触れてから1年近くが経とうとしているが、3ヶ月ほど前から本格的なシステムの開発を始めた。そういうわけでいろいろなところでつまづきまくっている。今回はWindowsサービスアプリケーションの開発。何とか道が見えたところで、記憶にとどめておくべきことをメモしておきたい。

クリティカルな部分をサービスに切り出して、必要なときだけGUIを立ち上げればよいような構造にしている。ゼロからの開発になるので、サービス・GUI・クラスライブラリを全部1つのソリューションの中で管理している。

サービスアプリ自体の開発はいったんやり方を見つければ簡単なんだけど、そこに行き着くまでが大変だった。ウェブ上にも古いやり方しか載っていない。installutil.exeとかコマンドラインツールを使う方法は古い。普通のアプリと同じ方法でセットアッププロジェクトを作ったあと、カスタム動作を設定するだけだ。このカスタム動作の設定の仕方は次のとおり。

1. ソリューションエクスプローラから、セットアッププロジェクトを右クリック->表示->カスタム動作

2. 開いた画面で、カスタム動作アイコンを右クリック->カスタム動作の追加

3. 開いたダイアログで、アプリケーションフォルダをダブルクリック。
 

4. アプリケーションのプライマリ出力を選択してOK。

5. 2の画面の各アクションにプライマリ出力が追加される。各アクション(イベント)のコールバックはProjectInstaller.csに自動的に実装される。
 

私は、これにインストール時に起動、アンインストール時に停止するアクションを加えた。ProjectInstallerのデザイナのイベントプロパティから、AfterInstall, BeforeUninstallイベントハンドラを追加する。ソース全体は以下のようになった。

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Linq;
using System.ServiceProcess;
using System.Windows.Forms;


namespace SignalService
{
    [RunInstaller(true)]
    public partial class ProjectInstaller : Installer
    {
        public ProjectInstaller()
        {
            InitializeComponent();
        }

        public override void Install(IDictionary stateSaver)
        {
            base.Install(stateSaver);
        }

        public override void Commit(IDictionary savedState)
        {
            base.Commit(savedState);
        }

        public override void Rollback(IDictionary savedState)
        {
            base.Rollback(savedState);
        }

        public override void Uninstall(IDictionary savedState)
        {
            base.Uninstall(savedState);
        }

        private void ProjectInstaller_AfterInstall(object sender, InstallEventArgs e)
        {
            ServiceController controller = new ServiceController(serviceInstaller1.ServiceName);
            controller.Start();
        }

        private void ProjectInstaller_BeforeUninstall(object sender, InstallEventArgs e)
        {
            ServiceController controller = new ServiceController(serviceInstaller1.ServiceName);
            try
            {
                controller.Stop();
            }
            catch (Exception)
            {
            }
        }
    }
}

過去にうちの先輩がC++で開発した、リモートデバッグ機能付きのロギングDLLを使おうとしたのだが、実はサービスアプリケーションで使うのは初めてだった。DllImportして使うわけだが、サービスが起動しているときに、GUIからもこのDLLを呼ぶと、System.DllNotFoundExceptionが発生する。サービスでなければ2つのアプリケーションから同時に使うことは問題なかったので、多分ユーザ権限の問題なのだろう。サービスはLocalSystemで動作させていた。このDLL特有の問題なのかもしれない。

また、ログファイルの出力先をカレントディレクトリのlogフォルダ(“.log”)としていたのだが、なぜかファイルが書かれない。絶対パスを指定するとちゃんと書かれるので、Program Filesに書くには特別な権限が必要なんだろうかと想像したりもしたが、デバッガをアタッチしてみると、実は単にカレントディレクトリが C:WINDOWSsystem32 になっていることが分かった。そこには普通にログが書き出されていた。以下のようにカレントディレクトリを移動することで解決した。

        private void MoveCurrentDirectory()
        {
            Assembly exe = Assembly.GetEntryAssembly();
            FileInfo exeFileInfo = new FileInfo(exe.Location);
            Directory.SetCurrentDirectory(exeFileInfo.DirectoryName);
        }

結局このDLLはサービスで使うと他で使えないようだったので、サービス用のロガーはC#で書いた。たいした機能はないが、キューイングして別スレッドで遅延書き込みみたいなことを実装した。リモートデバッグはとりあえず、Debug.Printで我慢する。

WCFでエンドポイントを追加したらエラーになって起動できなかった

スクリーンキャストを見ながらWCFのデモを打ち込んでいたところ、2番目のデモでいきなり躓いた。

“System.InvalidOperationException: 構成で指定された X.509 証明書 ID を読み込めません。”

エンドポイントのIDタブには、mexのだけDns=localhostになっていたので、ためしに他のにも入力してみたらうF5で動くようになった。

 

じっさいのところ、Dnsはたぶん関係なくて、このタブで設定されるCertificateReferenceの部分が、App.configにはデフォルトで追加されないので、このタブを修正したことで追加されたらしい。

結局Dnsのlocalhostは元通り空白に戻したが、ちゃんと動いている。まあ、不要な値は追加しないということで、バグではないんだろうなあ、偶然でなきゃ絶対気づけなかったと思うけど。

Hatena タグ: ,,