タグ:.Net ( 213 ) タグの人気記事
(.Net)特定のターミナルサーバ上のユーザにメッセージを送信する
.Netアプリケーションから任意のWindowsユーザにメッセージを送信する方法です。

他のコンピュータやターミナルサービス利用中のユーザにメッセージを送信するには msg コマンドが使えます。
(学生時代 net send というコマンドで他のWindowsPCにメッセージを送れることを知ってよく遊んでたんですが、今は net send コマンドの代わり msg コマンドを使うようです。Vista以降は net send コマンド自体が使えないようですし。。)

ちなみに、Windows7やVistaではターミナルサービスセッションに対するリモートからのRPC接続が無効に設定されているようなので、普通にはメッセージを送れないようです。
クライアントPCのレジストリで HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server キーの AllowRemoteRPC の値を 1 にするとRPC接続が有効にされれるようです。このあたりの詳しい話は【Management】Msgコマンドを使用すると「セッション名の取得エラー5」 - フィールドSEあがりの安納ですが参考になります。


msg コマンドは、ユーザ or セッションID or セッション名 でどのセッションに送信するかや、どのコンピュータに送信するかなどが指定できます。(どのコンピュータに送るかについて、ホスト名でなくてもIPアドレスでもOKでした。)
ただし、そのセッションに対するアクセス権を持っていないと「エラー [5]:アクセスが拒否されました。」となってしまいます。
まぁドメイン管理者権限なら大抵大丈夫なはずです。

msg コマンドでデフォルトの設定で送信すると、どうやら60秒間しかメッセージが表示されませんでした。ということで、1分では足りない場合は /time:秒数 で指定した方がよさそうです。

また、msgコマンドでアクセス権等不足してたり送った先のユーザが既にログアウトしてたりとエラーがあった場合ですが、どうやらエラーがあってもプロセスの戻り値は0のようです。
なので、エラーがあったかどうかは標準エラー出力からとらないとわかりません。
で、.Netでコンソールアプリケーションの標準出力や標準エラー出力を取得する方法ですが、Process.StandardError プロパティで標準エラー出力、Process.StandardOutput プロパティで標準出力の値が取れるようです。
ただし、Process.StandardError プロパティにアクセスするには ProcessStartInfo.RedirectStandardError を true に、Process.StandardOutput プロパティにアクセスするには、ProcessStartInfo.RedirectStandardOutput を true にしないといけないようです。
(上記の方法は同期式の方法と呼ばれており、処理中プロセスの標準出力を取ろうとしたりするとデッドロックになってしまう可能性があるようです。その場合はイベントハンドラを設定する非同期式を使うことが勧められています。)


ということで下記が.Netからmsgコマンドをキックしてメッセージを送信するコードです。コマンドプロンプトウィンドウは表示しないようにしてます。(C#)

/// <summary>
/// メッセージを送信する
/// </summary>
/// <param name="strUserName">ユーザ名</param>
/// <param name="strHostName">コンピュータ名orIPアドレス</param>
/// <param name="strMsg">メッセージ</param>
/// <param name="iDispTime">メッセージ表示秒数</param>
/// <returns>エラーメッセージ(nullの場合は正常)</returns>
public static string SendMessage(string strUserName, string strHostName, string strMsg, int iDispTime){
//プロセスオブジェクト生成
Process proc = new Process();
// 起動するアプリケーションを設定する
proc.StartInfo.FileName = "msg.exe";
//引数指定
proc.StartInfo.Arguments = string.Format("{0} /server:{1} /time:{2} {3} ", strUserName, strHostName, iDispTime.ToString() , strMsg);
// ウィンドウを作成しないかどうかを設定する (初期値 false)trueにするとウィンドウ表示なしとなる
proc.StartInfo.CreateNoWindow = true;
// シェルを使用するかどうか設定する (初期値 true)falseにするとコンソールウィンドウが表示なしとなる
proc.StartInfo.UseShellExecute = false;
// 起動できなかった時にエラーダイアログを表示するかどうかを設定する (初期値 false)
proc.StartInfo.ErrorDialog = false;
//標準エラー出力を取得できるようにする。
proc.StartInfo.RedirectStandardError = true;
 
// 起動する
proc.Start();
//起動したプロセスが終了するまで待つ。1秒のみ
proc.WaitForExit(1000);
 
//標準エラー出力取得
if (proc.HasExited){
string strErr = proc.StandardError.ReadToEnd();
strErr = strErr.Replace("\r\n", "");
if (String.IsNullOrEmpty(strErr)){
return null; //正常終了
}
return strErr; //異常終了 エラーメッセージ返す
}
return "タイムアウトしました。";
}

ただドメインのAdministrator以外だとアクセス権の問題で送信できないことがあるので、管理者ユーザ以外で使う場合は(.Net)別ユーザでプロセスを起動するで書いたようにAdministrator権限でmsgをキックするようにするといいと思います。

参考:
msg - Windowsコマンド集:ITpro
[Terminal Server コマンド] MSG
【Windows7/Vista】ポップアップメッセージを送りたい(net send あらため msg.exe) - フィールドSEあがりの安納です - Site Home - TechNet Blogs
コマンド・プロンプトでファイル共有を管理する - @IT コマンドで管理する場合の情報です。
DOS「メッセージ送信」コマンド(Hishidama's Windows DOS command "msg" Memo)
[.NET] コンソールアプリケーションの標準入出力をリダイレクトする - あおいるかの倉庫
[PR]
by jehoshaphat | 2011-06-06 23:42 | .Net開発 | Trackback | Comments(0)
(.Net)LINQのクエリ構文とメソッド構文(ラムダ式)を使ってみた
.Net Framework 3.5(C#3.0)から追加されたLINQですが、ようやく使ってみることにしました。

SQLServer等のDBへの問い合わせに使えるようですが、今回はコレクションでの検索がメインです。

とりあえず基本的なことは、@IT:LINQ(リンク)の基礎知識と、VB はじめてのLINQ - LINQ, From, Where, Select, OfTypeで頭に詰め込みます。

単純なコレクション検索だと下記のような感じになります。

public partial class Form1 : Form {
private void button1_Click(object sender, EventArgs e) {
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() { Name = "a", Id = 1 });
lst.Add(new TestClass() { Name = "b", Id = 2 });
lst.Add(new TestClass() { Name = "c", Id = 3 });
lst.Add(new TestClass() { Name = "d", Id = 5 });
lst.Add(new TestClass() { Name = "e", Id = 5 });
lst.Add(new TestClass() { Name = "f", Id = 4 });
lst.Add(new TestClass() { Name = "g", Id = 5 });
 
//LINQで検索 IDが5のものを探す
var query = from res in lst
where res.Id == 5
select res; //今回は TestClass オブジェクトを返すが、res.Name とかすることも可能
//foreach句で結果が参照できる
foreach (var res in query)
{
Console.WriteLine(res.Name + " , " + res.Id);
}
}
}
 
 
public class TestClass {
public string Name { get; set; }
public int Id { get; set; }
}

from in 句で検索元データを指定し、where で条件、select で結果として欲しいオブジェクトを指定します。
SQLチックで簡単にデータ抽出できますね。


しかし、いざ実用しようかと思うと、問題になったのがクエリの条件が動的になる場合です。
SQLだと string 結合するだけでよかったんですが、上記のようなLINQの書き方では動的には条件設定ができません。

LINQを活用した簡単XMLデータベース・アプリケーションにて、動的に条件を設定する方法が書いてあるんですが、ラムダ式やら式ツリーやらいまいちわかりません。


ということでLINQ 超入門(4) クエリの文法 where句 - 気楽なC#工房を参考にしました。

実際の条件に一致するかどうかのフィルタ部分を別メソッドに持たせる方法です。
こんな感じです。

public partial class Form1 : Form {
private void button1_Click(object sender, EventArgs e) {
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() { Name = "a", Id = 1 });
lst.Add(new TestClass() { Name = "b", Id = 2 });
lst.Add(new TestClass() { Name = "c", Id = 3 });
lst.Add(new TestClass() { Name = "d", Id = 5 });
lst.Add(new TestClass() { Name = "e", Id = 5 });
lst.Add(new TestClass() { Name = "f", Id = 4 });
lst.Add(new TestClass() { Name = "g", Id = 5 });
 
//LINQで検索 IDが5のものを探す
var query = from res in lst
where Filter(res,null,5)
select res;
//foreach句で結果が参照できる
foreach (var res in query) {
Console.WriteLine(res.Name + " , " + res.Id);
}
}
 
//実際に条件を判定するメソッド。引数がnullでない時にチェックする。
private bool Filter(TestClass cls, string filterName, int? filterId) {
if (filterName != null && !cls.Name.Contains(filterName)){
return false;
}
if (filterId.HasValue && cls.Id != filterId.Value) {
return false;
}
return true;
}
}
 
public class TestClass {
public string Name { get; set; }
public int Id { get; set; }
}

ただ上記の Filter メソッドの場合、条件がANDだけになってしまいます。
OR条件にするは Filter メソッドを下記のようにします。

private bool Filter(TestClass cls, string filterName, int? filterId) {
if (filterName != null && cls.Name.Contains(filterName)) {
return true;
}
if (filterId.HasValue && cls.Id == filterId.Value) {
return true;
}
return false;
}

ただ複雑な条件は指定できないので、やはりラムダ式で式ツリーを生成する方法じゃないとダメでしょうかね。。。


ところで、LINQですが上記のようなクエリ構文とは別にメソッド構文というものも有ります。
両者のコードを下記に載せてみます。(文字数の関係でハイライトOFFです。)

//テスト用のデータ生成
List lst = new List();
lst.Add(new TestClass() { Name = "a", Id = 1 });
lst.Add(new TestClass() { Name = "b", Id = 2 });
lst.Add(new TestClass() { Name = "c", Id = 3 });
lst.Add(new TestClass() { Name = "d", Id = 5 });
lst.Add(new TestClass() { Name = "e", Id = 5 });
lst.Add(new TestClass() { Name = "f", Id = 4 });
lst.Add(new TestClass() { Name = "g", Id = 5 });

//検索条件の値。
int iFind=5;
string strFind = "a";

//LINQ クエリ構文での指定。Id が 5 またはNameが a のデータ
var query = from res in lst
where res.Id == iFind ||
res.Name.Equals("a")
select res;

//LINQ メソッド構文での指定。Id が 5 またはNameが a のデータ
var query2 = lst.Where(pram => pram.Id == iFind ||
pram.Name.Equals("a"))
.Select(pram => pram);


メソッド構文ではラムダ式を使っています。

ラムダ式もよくわかってませんが、C#2.0でデリゲートを簡単にかけるために匿名メソッドができました。その匿名メソッドをさらに簡潔に書けるようにしたもののようです。

=>演算子(goes to演算子)は「~に入力」と読むようですね。
左辺がデリゲートとして呼び出されるメソッドのパラメータが入るようです。(パラメータが複数場合は()でくくる必要がありますが、今回は一つなのでくくっていません)
右辺には式や文が入ります。ただLINQで使う場合は式が多いんじゃないかと思います。
ラムダ式の詳細は、@IT:C#ラムダ式 基礎文法最速マスタ@IT:第1回 ラムダ式 が分かりやすいです。

LINQのクエリ構文もメソッド構文もコンパイルすると全く同じコードになるようです。(@IT:第8回 LINQメソッド形式編が参考になります。MS的には、LINQ クエリ構文とメソッド構文 (C#)で「クエリ構文の方が単純で読みやすいため、クエリ構文を使用することをお勧めします。」とありました。)


ただ、オブジェクトLISTのWhereメソッドで検索する時、匿名メソッドだと無駄なコード吐き出すので、LINQのラムダかメソッド構文がいいということが、匿名メソッドとラムダ、そして LINQ の違い - vol 1,匿名メソッドとラムダ、そして LINQ の違い - vol 2で論じられてました。


言語の進歩に取り残されてる感が漂う3流PGでした。

追記:
動的に条件を設定する方法ですが、(.Net)LINQでラムダ式を動的に生成するに書いてみました。
[PR]
by jehoshaphat | 2011-06-06 22:52 | .Net開発 | Trackback | Comments(0)
(.Net)WindowsAPIのエラーコードからメッセージを取得する
.NET から WindowsAPI(Win32API) を呼び出した場合大抵の Win32API は戻り値としてエラーコードを返します。
このエラーコードはWin32 エラー コードと呼ばれてるようですが、System.ComponentModel.Win32Exception クラスの Message メソッドを使うとエラーコードのメッセージ文字列が取得できるようです。

ということで、参考先のコードそのままですがWin32エラーコードからエラーメッセージと16進数のエラーコードを返すメソッドです。(C#)

/// <summary>Win32エラーコードから、エラーを説明するメッセージと16進数表現を返します。</summary>
/// <param name="code">Win32エラーコード</param>
/// <returns>メッセージ+16進数</returns>
public static string FormatMessage(int code)
{
byte[] b = BitConverter.GetBytes(code);
Array.Reverse(b);//リトルエンディアンなので
string bs = "0x" + BitConverter.ToString(b, 0).Replace("-", "");
 
return string.Format("{0}({1})", new System.ComponentModel.Win32Exception(code).Message, bs);
}


参考:
C#メモ - shise.net
以下Win32のエラーコードを載せているサイトです。
Windowsシステムエラーコード一覧
Win32エラーコード一覧
Network Management Error Codes (Windows) ネットワーク系のみ。
[PR]
by jehoshaphat | 2011-06-06 22:36 | .Net開発 | Trackback | Comments(0)
(.Net)C#3.0からオブジェクトのコレクションの初期化が簡潔に書けるようになってた

C#3.0(.Net Framwork3.5)からオブジェクトの初期化が簡単に書けるようになってたようです。

以前(C#2.0)はこのように書いてました。

public class TestClass{
public string Name { get; set; }
public int Id { get; set; }
}
 
 
private void button1_Click(object sender, EventArgs e){
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() );
lst[0].Id = 2;
lst[0].Name = "a";
lst.Add(new TestClass() );
lst[1].Id = 1;
lst[1].Name = "b";
}

しかし、C#3.0からは下記のようにすることで、インスタンス作成時に1行でPublicメンバの初期化ができます。

private void button1_Click(object sender, EventArgs e){
//テスト用のデータ生成
List<TestClass> lst = new List<TestClass>();
lst.Add(new TestClass() { Name="a",Id = 2 });
lst.Add(new TestClass() { Name="b",Id = 1 });
}


ただこのようなインスタンス作成時の初期化はそれ用のコンストラクタを作成して使うべきだと思うんですがどうでしょう。。


また、インスタンス作成時だけでなくコレクションでも初期化が簡潔に書けるようになったようです。
上記で取り上げたオブジェクトイニシャライザとコレクション初期化子を組み合わせると下記のようになります。(C#)

private void button1_Click(object sender, EventArgs e){
List<TestClass> dataSource2 = new List<TestClass>() {
new TestClass() { Name="a",Id = 2 },
new TestClass() { Name="b",Id = 1 }
};
}


参考:
CSharp 3.0 - CSharp - Jankh メモ Wiki
[PR]
by jehoshaphat | 2011-06-06 22:32 | .Net開発 | Trackback | Comments(0)
(.Net)アプリ上で共有フォルダの認証を行いファイルアクセスをする
ドメインに参加してないPC上で動く.Netアプリケーションから、ドメイン内のファイルサーバにアクセスしたいと言う要件です。

当然.Netアプリケーション内で共有フォルダの認証を行わなくてはなりません。


で、調べるとプログラム上で認証情報(アカウントとパスワード)をセットして、ネットワーク上に共有されたファイルをコピーする - ComponentGeek Articleにどんぴしゃな情報が載ってました。

.Net のクラスライブラリ自体にはそのような機能は提供されてないようなので、Windows API を使うことになるようです。

WNetAddConnection2 というAPIと WNetAddConnection3 というAPIがあるようです。違いは後者は"ネットワーク資源のプロバイダがダイアログボックスのオーナーウィンドウとして利用できるウィンドウのハンドルという1つのパラメータが追加されている"だけのようです。要は3の方は認証ダイアログウィンドウのハンドルを指定することができるようです。
(WNetAddConnectionってのもあるようですが16bit版みたいです。)


ちなみに、WNetAddConnection2 で接続する前にすでに対象の共有フォルダに接続されている場合はERROR_SESSION_CREDENTIAL_CONFLICT(1219)のエラーが返ります。
なので一旦 WNetCancelConnection2 で現在の接続を切断してからつなぐ方が安心かもしれません。

ということで一旦切断してから接続するサンプルコードです。(C#)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
 
public partial class Main : Form
{
//接続切断するWin32 API を宣言
[DllImport("mpr.dll", EntryPoint = "WNetCancelConnection2", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
private static extern int WNetCancelConnection2(string lpName, Int32 dwFlags, bool fForce);
 
//認証情報を使って接続するWin32 API宣言
[DllImport("mpr.dll", EntryPoint = "WNetAddConnection2", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
private static extern int WNetAddConnection2(ref NETRESOURCE lpNetResource, string lpPassword, string lpUsername, Int32 dwFlags);
//WNetAddConnection2に渡す接続の詳細情報の構造体
[StructLayout(LayoutKind.Sequential)]
internal struct NETRESOURCE
{
public int dwScope;//列挙の範囲
public int dwType;//リソースタイプ
public int dwDisplayType;//表示オブジェクト
public int dwUsage;//リソースの使用方法
[MarshalAs(UnmanagedType.LPWStr)]
public string lpLocalName;//ローカルデバイス名。使わないならNULL。
[MarshalAs(UnmanagedType.LPWStr)]
public string lpRemoteName;//リモートネットワーク名。使わないならNULL
[MarshalAs(UnmanagedType.LPWStr)]
public string lpComment;//ネットワーク内の提供者に提供された文字列
[MarshalAs(UnmanagedType.LPWStr)]
public string lpProvider;//リソースを所有しているプロバイダ名
}
 
 
private void button1_Click(object sender, EventArgs e)
{
string destFilePath = @"C:\test.txt"; // コピー先のローカルパス
string sourceFilePath = @"\\filesv\共有フォルダ\test.txt"; // コピー対象の共有されたファイルのUNCパス
string shareName = @"\\filesv\フォルダ"; // 共有パス \\sparePC\C$などもOK
try
{
//とりあえずコピーできるかやってみる
System.IO.File.Delete(destFilePath);
System.IO.File.Copy(sourceFilePath, destFilePath);
}
//認証に失敗すると UnauthorizedAccessException が発生する(Exceptionでキャッチしてもいいかも)
catch (UnauthorizedAccessException)
{
// 接続情報を設定
NETRESOURCE netResource = new NETRESOURCE();
netResource.dwScope = 0;
netResource.dwType = 1;
netResource.dwDisplayType = 0;
netResource.dwUsage = 0;
netResource.lpLocalName = ""; // ネットワークドライブにする場合は"z:"などドライブレター設定
netResource.lpRemoteName = shareName;
netResource.lpProvider = "";
 
string password = "passwd";
string userId = @"hogedomain.local\hoge";
 
int ret = 0;
try
{
//既に接続してる場合があるので一旦切断する
ret = WNetCancelConnection2(shareName, 0, true);
//共有フォルダに認証情報を使って接続
ret = WNetAddConnection2(ref netResource, password, userId, 0);
}
catch (Exception)
{
//エラー処理
}
Console.WriteLine(ret);
//ファイルコピー
System.IO.File.Delete(destFilePath);
System.IO.File.Copy(sourceFilePath, destFilePath);
 
}
Console.WriteLine("終了");
Console.ReadLine();
}
}

なお NETRESOURCE 構造体に設定できるのは下記の値になるようです。

dwScope(列挙の範囲)
RESOURCE_CONNECTED(0x1) 現在接続されたリソースを列挙する。dwUsageメンバーを指定できない。
RESOURCE_GLOBALNET(0x2) ネットワークに関するすべてのリソースを列挙する。
RESOURCE_REMEMBERED(0x3) 接続を列挙する。dwUsageメンバーを指定できない。


dwType(リソースタイプ)
RESOURCETYPE_ANY(0x0) すべてのリソース
RESOURCETYPE_DISK(0x1) ディスクリソース
RESOURCETYPE_PRINT(0x2) プリンタリソース

dwDisplayType(表示オブジェクト)
RESOURCEDISPLAYTYPE_DOMAIN(0x1) ドメインオブジェクトを表示する
RESOURCEDISPLAYTYPE_SERVER(0x2) サーバオブジェクトを表示する
RESOURCEDISPLAYTYPE_SHARE(0x3) シェアオブジェクトを表示する
RESOURCEDISPLAYTYPE_GENERIC(0x0) オブジェクトの表示は重要ではないことを示す

dwUsage(リソースの使用方法)
RESOURCEUSAGE_CONNECTABLE(0x1) 接続可能なリソースであることを示す。lpRemoteNameメンバーによって示された名前はWNetAddConnection機能に通過できる。
RESOURCEUSAGE_CONTAINER(0x2) リソースはコンテナリソースです。lpRemoteNameメンバーによって示された名前はWNetOpenEnum機能に通過できる。


MSDNには構造体に指定できる値を書いてるんですが、定数で書いてるんですよね。.Netだとその定数が使えないので実値を指定しないといけません。.NetでもWindowsの定数使えないものですかね。。。


APIの戻りのエラーコードはWin32エラーコード一覧が参考になります。
[PR]
by jehoshaphat | 2010-12-08 00:12 | .Net開発 | Trackback | Comments(0)
.Net(C#)アプリからExcelのVBAを呼び出したい(遅延バインディング)
.NetアプリからExcelのVBAを呼び出したい(事前バインディング) で参照設定からCOMのOffice操作DLLを呼び出す方法を書きました。(あらかじめ呼び出すオブジェクトのタイプライブラリを参照してコンパイル時に型チェックをするこの方法を事前バインディングと言います。)

しかしこの方法では参照設定したバージョンのExcelが入っているPCでしか実行できません。

対策としては、実行時にバインドする遅延バインディングを用います。

事前バインディングのコードと比較しながら遅延バインディングのコードを下記に書いてみます。
(コメント部分のコードが事前バインディングになります。)

using System.Runtime.InteropServices;
using System.Reflection;
 
public ExeMacro()
{
 
//Excelマクロファイルパス
string strMacroPath = @"D:\test.xls";
 
// Excel操作用COMオブジェクトを生成する
//ApplicationClass oExcel = new ApplicationClass();
object oExcel = CreateObject("Excel.Application");
 
//ワークブックコレクションオブジェクトを生成する。
//Workbooks oBooks = oExcel.Workbooks;
object oBooks = oExcel.GetType().InvokeMember("Workbooks", BindingFlags.GetProperty, null, oExcel, null);
 
//Excelファイルのオープン
//Workbook oBook = oBooks.Open(strMacroPath);
object oBook = oBooks.GetType().InvokeMember(
"Open", BindingFlags.InvokeMethod, null,
oBooks, new object[] {
strMacroPath
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
, Type.Missing
});
 
// Excelファイルの表示
//oExcel.Visible = true;
oExcel.GetType().InvokeMember("Visible", BindingFlags.SetProperty, null, oExcel, new object[] { true });
 
//マクロ実行(Testというサブプロシージャを実行する)
//oExcel.Run("Test");
oExcel.GetType().InvokeMember("Run", BindingFlags.InvokeMethod, null, oExcel, new object[] { "Test" });
 
//閉じる
//oBook.Close(false);
oExcel.GetType().InvokeMember("Quit", System.Reflection.BindingFlags.InvokeMethod, null, oExcel, null);
//COM解放
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(oBook);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(oBooks);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(oExcel);
oBook = null;
oBooks = null;
oExcel = null;
}
 
/// <summary>
/// COMオブジェクトへの参照を作成および取得します
/// </summary>
/// <param name="progId">作成するオブジェクトのプログラムID</param>
/// <param name="serverName">
/// オブジェクトが作成されるネットワークサーバー名
/// </param>
/// <returns>作成されたCOMオブジェクト</returns>
public static object CreateObject(string progId, string serverName)
{
Type t;
if (serverName == null || serverName.Length == 0)
t = Type.GetTypeFromProgID(progId);
else
t = Type.GetTypeFromProgID(progId, serverName, true);
return Activator.CreateInstance(t);
}
 
/// <summary>
/// COMオブジェクトへの参照を作成および取得します
/// </summary>
/// <param name="progId">作成するオブジェクトのプログラムID</param>
/// <returns>作成されたCOMオブジェクト</returns>
public static object CreateObject(string progId)
{
return CreateObject(progId, null);
}


使い終わったら、Quitメソッドで、Excelファイルを閉じて、Marshal.FinalReleaseComObjectメソッドを使って、COMオブジェクトを必ず解放するようにしましょう。
上記のコードで事前と遅延バインディングの比較をしてもらうとわかるんですが、基本的に oExcel.GetType().InvokeMember メソッドでExcel内のメソッド実行したりプロパティを変更できるようです。(たしかに型が分からないわけですがら、リフレクションを使ってメソッド名やプロパティを指定しないといけませんね。)


VB.Netだとより簡単に遅延バインディング(実行時バインディング)ができるんですが、C#は結構面倒ですね。(VBはVB6時代から実行時バインディング使ってましたからね。)

参考:
(VB.Net)COMオブジェクトを解放する
MSサポート:Visual C# .NET で Office オートメーション サーバーをバインドする方法
とほほな日々 備忘録: Excel 遅延バインディング(LateBinding)
C#から遅延バインディングでEXCELのシート保護をする - とりあえずですけれども - 1981s
リフレクションを利用したレイトバインディングでExcelファイルを開く - Bug Catharsis
(続)リフレクションを利用したレイトバインディングでExcelファイルを開く - Bug Catharsis
Excelの解放: DOBON.NETプログラミング掲示板過去ログ
圧砕の雑記的な何か: 遅延バインディング
[PR]
by jehoshaphat | 2010-11-27 13:30 | .Net開発 | Trackback | Comments(0)
.Net(C#)アプリからExcelのVBAを呼び出したい(事前バインディング)
.NetアプリケーションからExcel2007のVBA(マクロ)標準モジュール内のサブプロシージャを呼び出す方法です。


まず、参照設定で、COMの Microsoft Excel 12.0 Object Library を追加します。
(12.0とかはExcelのバージョンで変わってきます。)

そして、下記のようなコードを書きます。(C#)
using Microsoft.Office.Interop.Excel;
 
//Excelマクロファイルパス
string strMacroPath = @"D:\test.xls";

 
//Excel操作用オブジェクト生成
ApplicationClass oExcel = new ApplicationClass();
//表示する
oExcel.Visible = true;
Workbooks oBooks = oExcel.Workbooks;

//Excelファイル開く
Workbook oBook = oBooks.Open(strMacroPath);
 
//マクロ実行(Testというサブプロシージャを動かす)
oExcel.Run("Test");
 
//終了処理(リソース解放)
oBook.Close(false);

System.Runtime.InteropServices.Marshal.ReleaseComObject(oBook);
oBook = null;
 
System.Runtime.InteropServices.Marshal.ReleaseComObject(oBooks);
oBooks = null;

 
oExcel.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcel);
oExcel = null;

簡単ですね。といってもほぼ参考先まるままですが。。
しかし、この方法ではExcel2007が入っているPCでしか実行できません。
例えばExcel2003が入っているPCとかだと、エラーとなります。
ということで、どのバージョンでのExcelでも実行できる方法は次の記事で書きます。

参考:
全ては時の中に… : 【VB.NET】VB.NETからExcelのVBA(マクロ)を実行する
[PR]
by jehoshaphat | 2010-11-27 13:25 | .Net開発 | Trackback | Comments(0)
(.Net).NetアプリケーションからAccess(MDB)のテーブルをExcelファイルにエクスポート
Accessにはテーブルを各種形式にファイルにエクスポートできる機能があります。
(テーブル選択し、ファイルメニュー→エクスポートのことです。)

さて、これを .Net のアプリケーションからAccessを起動しなくてもExcelファイル(xls)にエクスポートしてやろうと言うのが今回のしたいことです。

●SQLクエリを使う方法
まず JETデータベース(MDB) のSQLクエリ自身にエクスポートする機能があるようです。
下記のようなSQL構文になります。

SELECT * ITNO エクスポート先ファイル名 FROM テーブル名

.Netからは OLEDB 経由で上記構文のSQLを実行すればいいだけです。
下記にサンプルソースを載せます。(C#)

//OLEDB接続オブジェクト生成
System.Data.OleDb.OleDbConnection cn = new System.Data.OleDb.OleDbConnection();
cn.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=\"D:\\test.mdb\";";
//OLEDBコマンドオブジェクト生成
System.Data.OleDb.OleDbCommand cmd = new System.Data.OleDb.OleDbCommand();
cmd.Connection = cn;
 
//SQLクエリでエクスポートを指定
string strSQL = "SELECT * INTO [Excel 8.0;Database=D:\\test0.xls].[テストシート] FROM [tblTest]";
cmd.CommandText = strSQL;
 
cn.Open();
//実行
cmd.ExecuteNonQuery();
cn.Close();

上記を実行すると、D:\test.mdb 内の "tblTest" テーブルが D:\test0.xls ファイルの "テストシート" に出力されます。
この時 JET エンジンは Microsoft Jet 4.0 OLE DB プロバイダを使ってエクスポートするようですね。
対応するExcelファイルファイルのバージョンは下記のとおりです。
Excel 3.0
Excel 4.0
Excel 5.0(Excel95)
Excel 8.0(Excel97-2003)

(なお、Excel 12.0 以降(Excel2007)は JET 4.0 OLE DB じゃサポートしてないっぽいです。Microsoft Office 12.0 Access Database Engine OLE DB Providerをいれれば新形式も扱えるようになということがサンプルプログラム集 [631_ADOでAccess2007用のJETデータベースを扱う]に書かれていました。)


さて、まずこの方法で試みたわけですが、ExecuteNonQuery メソッドで下記のような例外が発生してしまいます。

System.Data.OleDb.OleDbException はハンドルされませんでした。
Message=数値フィールドがオーバーフローしました。
Source=Microsoft JET Database Engine
ErrorCode=-2147467259
StackTrace:
場所 System.Data.OleDb.OleDbCommand.ExecuteCommandTextErrorHandling(OleDbHResult hr)
場所 System.Data.OleDb.OleDbCommand.ExecuteCommandTextForSingleResult(tagDBPARAMS dbParams, Object& executeResult)
場所 System.Data.OleDb.OleDbCommand.ExecuteCommandText(Object& executeResult)
場所 System.Data.OleDb.OleDbCommand.ExecuteCommand(CommandBehavior behavior, Object& executeResult)
場所 System.Data.OleDb.OleDbCommand.ExecuteReaderInternal(CommandBehavior behavior, String method)
場所 System.Data.OleDb.OleDbCommand.ExecuteNonQuery()
.....

調べてみたんですがエクスポート時におきる現象はどうもあまりないようです。
Accessのファイルメニューからエクスポートした時はうまく行くんですが。。。
簡単な mdb を作るとうまくいきました。
今回ターゲットにしてる mdb は数万件のデータが入っており、mdb にしてはそこそこ大きいので、どっかで型の不整合があるのかもしれません。
原因追究したかったんですが、時間が無いので別の方法をとることにしました。

参考:
きままにスクリプト
サンプルプログラム集 [623_ADOでJETデータベースのエクスポート]
MSサポート:ADO を使用して ADO データ ソースから Excel にデータを転送する方法


●VBScriptを使う方法
VBScript(WSH)から Access の TransferSpreadsheet メソッドを呼び出してもExcel形式にエクスポートできるようなので、その方法を試してみました。
下記のようなスクリプトになります。

'モード指定 1がエクスポートみたい(0だとインポート)
Const acExport = 1
'エクスポートするExcelファイルのバージョン
Const acSpreadsheetTypeExcel9 = 8
'Access操作用オブジェクト生成
Set objAccess = CreateObject("Access.Application")
'対象となるMDB指定
objAccess.OpenCurrentDatabase "D:\test.mdb"
'Excelファイルにエクスポート
objAccess.DoCmd.TransferSpreadsheet acExport, acSpreadsheetTypeExcel9, "tblTest", "D:\test1.xls", True

上記のコードはMS:Hey, Scripting Guy! Access データベースのテーブルをワークシートとして保存する方法はありますかまるままです。
ただSQLクエリの時と違って、Excelのシート名の指定はできません。エクスポートしたテーブル名がシート名となります。(Accessファイルメニューで行うときと同じですね。)

後はこのスクリプトを .Net アプリケーションから実行してやればいいだけです。
上記のスクリプトが export.vbs というファイル名で、exe と同じパスにあるとした場合、こんな感じです。(C#)

using System.Diagnostics;
 
string strScrptPath = System.Windows.Forms.Application.StartupPath + "\\export.vbs";
//VBScript起動(wscriptなのはWaitForInputIdleを使えるようにするため)
Process prc = Process.Start("wscript.exe", "\"" + strScrptPath + "\"");
//↑で起動したプロセスが終わるまで待つ。
while (true)
{
System.Threading.Thread.Sleep(500);
try {
Process.GetProcessById(prc.Id);
} catch (Exception){
break;
}
}
//prc.WaitForInputIdle();

プロセス終了の判定ですが、本当はWaitForInputIdleメソッド使いたかったんですが、どうやらwscriptのようなアクティブウィンドウを持たない場合は使えないようです。
仕方ないので、無限ループで回して、プロセスIDが無くなったらループ抜ける仕組みにしました。

これでようやく要件どおりに動くようになりました。


参考:
Excelへデータを出力する方法 - TransferSpreadsheetメソッド:SampleFile126
[VBA]AccessからExcelにデータをインポートする方法 (ADO編) - DQNEO起業日記 別の方法としてExcelのVBAからmdbひらいてコピペする方法もあるようです。
MSサポート:Visual Basic .NET と ADO.NET を使用して Excel ブックのレコードの取得と変更を行う方法 JET OLE DBプロバイダを使って.NetからExcelを操作する方法もあるようです。
[PR]
by jehoshaphat | 2010-11-27 13:21 | .Net開発 | Trackback | Comments(0)
(.Net)DotNetZipでZIPファイルを作る時の注意

(.Net)MemoryStreamのデータをGZipStreamで圧縮するときの注意点..で、.Netは標準のクラスで、GZIP(.gz)が作れることを書きましたが、Windowsではあまり使われていない形式なのでちょっと不便です。

ところが、DotNetZipというライブラリを使えば非常に簡単にZIPファイルを作ることができます。

詳しい使い方はマイコミジャーナル:【ハウツー】C#でZIPファイルを扱えるDotNetZip (1) .NETで書庫を作成、圧縮を見てもらえばいいんですが、ちょっと圧縮でハマったことがあったのでメモ。

とりあえずファイル一つ圧縮できればいいので下記のようなコードを書きました。

using (ZipFile zip = new ZipFile(Encoding.GetEncoding("shift_jis")))
{
zip.CompressionLevel = CompressionLevel.BestCompression;
//ファイルを追加

zip.AddFile("databse.mdb");
zip.Save("test.zip");
}

これでできたZIPを確認すると、フォルダ構成が基の圧縮元があったファイルのパスになっているのです。
つまり、こんな感じ。

mydocuments/Visual Studio 2008/Projects/ZIPTest/ZIPTest/bin/x86/Debug

まぁ、上記マイコミジャーナルの記事にも書いてあることなんですが、AddFile メソッドの第二引数でパスを明示的にしてしないとこうなるようです。

ということで、ZIPのルートに圧縮元ファイル置きたかったので、こうしました。

zip.AddFile("databse.mdb","");

しかし、このライブラリはホント手軽に使えるので便利ですね。
[PR]
by jehoshaphat | 2010-11-01 21:25 | .Net開発 | Trackback | Comments(0)
(.Net)アカウトを指定してProcess.Startを使うときはWorkingDirectoryプロパティを指定しないといけない
共有フォルダ上で動く.Net WindowsFormアプリ(仮にapp1)を作成しています。
その共有フォルダから、別のサーバ上の共有フォルダ上の.Netコンソールアプリ(仮にapp2)をキックし、その.Netアプリから管理者ユーザ権限でバッチ(bat1)を起動させるというカラクリを作ってました。

ところが、app2からbat1を起動させる時に、app2の Process.Start 時に下記のような例外が発生しました。

ディレクトリ名が無効です。
場所 System.Diagnostics.Process.StartWithCreateProcess(ProcessStartInfo startInfo)
場所 System.Diagnostics.Process.Start()
場所 app1.Program.Main(String[] args)

で、調べてみたところ別ユーザでプロセスやファイルを実行する場合(ProcessStartInfo の UserName , Password を指定した場合)は、WorkingDirectory プロパティを設定し、作業フォルダを指定しないといけないようですね。
そのことは、ProcessStartInfo.WorkingDirectory プロパティ (System.Diagnostics)にも書いてありました。

ということで、上記のapp1,app2について別プロセスを起動する部分は下記のようにコーディングしました。(C#)

■app1
int iExitCode = 0;
try{
//プロセスオブジェクト生成
Process proc = new Process();
//個人フォルダ作成ツールのパスをセット
string strPath = Properties.Settings.Default.MakePersonalDirPath;
proc.StartInfo.FileName = strPath;
//コマンドライン引数はユーザ名
proc.StartInfo.Arguments = username;
//シェルを使わない。
proc.StartInfo.UseShellExecute = false;
//プロセス起動
proc.Start();
 
//起動したプロセスが終わるまで待つ。
proc.WaitForExit();
 
iExitCode = proc.ExitCode;
}catch{
//エラー表示
return;
}
 
if (iExitCode != 0){
//エラー表示
}



■app2
//コマンドライン引数取得
string[] cmds;
cmds = System.Environment.GetCommandLineArgs();
//プロセスオブジェクト生成
Process proc = new Process();
// 起動するアプリケーションを設定する(このapp2.exeと同じフォルダのバッチファイルを指定)
proc.StartInfo.FileName =System.Windows.Forms.Application.StartupPath + @"\bat1.bat";
//ワークディレクトリの設定(これを指定しないと"ディレクトリが無効です。"の例外になることがある。)
proc.StartInfo.WorkingDirectory = System.Windows.Forms.Application.StartupPath;
// コマンドライン引数を設定する
proc.StartInfo.Arguments = cmds[1];
// 新しいウィンドウを作成するかどうかを設定する (初期値 false)
proc.StartInfo.CreateNoWindow = false;
// シェルを使用するかどうか設定する (初期値 true)
proc.StartInfo.UseShellExecute = false;
// 起動できなかった時にエラーダイアログを表示するかどうかを設定する (初期値 false)
proc.StartInfo.ErrorDialog = false;
//プロセス実行時のドメイン名(ローカルユーザの場合はnullを指定)
proc.StartInfo.Domain = "hogedomain";
//プロセス実行時のユーザ名
proc.StartInfo.UserName = "administrator";
//ユーザのパスワード
string pswwd = "passwd";
//パスワード(ProcessStartInfo.PasswordはSecureStringで指定しないといけない)
SecureString password = new SecureString();
foreach (char c in pswwd.ToCharArray())
{
//SecureStringの文字追加
password.AppendChar(c);
}
//プロセス実行時のユーザのパスワード指定
proc.StartInfo.Password = password;
// 起動時のウィンドウの状態を設定する
proc.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; //通常
 
// 起動する
proc.Start();
//起動したプロセスが終わるまで待つ。(60秒待つ)
proc.WaitForExit(60000);


参考:
ネットワーク上にあるファイルの実行について
[PR]
by jehoshaphat | 2010-11-01 21:23 | .Net開発 | Trackback | Comments(0)