Oracle12C プラガブル・データベースの自動起動設定

こんにちは。山内です。
この前、Oracle12Cを開発用にPCへインストールしたのですが、インストールした翌日にPCを起動して開発を始めたところ、データベースに接続できない状態が発生いたしました。調査したところ、Oracle12Cのプラガブル・データベース(以下:PDB)は デフォルトではサーバーやPC起動時には自動で起動するようになっていないため、サーバーやPC起動時に手動で起動を行うか自動で起動するように設定が必要であることが判明しました。 同じようなことで困った人もいるかと思いますので、サーバーやPC起動時にPDBが自動で起動するように設定した方法を以下に記載いたします。


環境

Oracle12.1.0.2とOSはWindowsを使用することを前提に説明を行います。


PDBについて

Oracle Database 12cで機能追加されたのが、「マルチテナント・アーキテクチャ」です。マルチテナント・アーキテクチャは、マルチテナント・コンテナ・データベース(以下:CDB)というデータベースに含まれる1つ以上のPDBによって構成されます。スキーマや表領域やデータなどがセットになったものがPDBです。アップグレードやパッチ適用はCDBに適用を行うことでCDBに複数のPDBを構成している場合は1度の適用のみで済むことや必要なPDBのみ起動することができるのでリソース使用量の削減が可能です。ただし、OSの再起動時にはPDBは自動的に起動する設定になっていないので自動起動設定を行うためには以下で説明する設定などが必要になります。Oracle12Cのバージョンによって設定方法が異なりますのでバージョンごとの設定方法を以下で説明いたします。

Oracle12.1.0.2での設定方法

Oracle12.1.0.2以降では以下の設定でサーバーやPC起動時にPDB自動起動することが可能です。

1. sysdbaでsqlplusにログイン
sqlplus ユーザ名/PASS as sysdba;
2. 設定を行うPDBに接続先を変更
 alter session set container = PDB名;

接続するPDBを変更します。

 show con_name;

※show con_nameコマンドで接続先のPDBを確認することができます。

3.PDBを開く
 alter pluggable database PDB名 open;

PDBを起動します。

4.PDB自動起動設定

3の設定でPDBが起動されている状態で以下のコマンドを実行します。

 alter pluggable database PDB名 save state;

上記の設定を行うことにより、サーバーやPCの起動時にPDBが起動されるようになります。


Oracle12.1.0.1での設定方法

Oracle12.1.0.1では自動起動する設定を行うことができないため、 以下の方法でサーバーやPCの起動時にタスクスケジューラーのトリガーをシステム起動時にバッチを実行することで対応を行いました。

1.PDBを起動するコマンドを作成
 alter pluggable database PDB名 open;
 quit;
2.起動バッチ作成

1で作成したSQLを呼び出すバッチを作成します。

 sqlplus ユーザ名/PASS as sysdba @c:\oracle\PDBOPEN.sql
3.タスクスケジューラーの設定

タスクスケジューラーから(2)で作成したバッチファイルをシステム起動時に開始する設定を行います。 f:id:tac7:20151205135815p:plain
※「操作」タブで新規タクスを作成します。

f:id:tac7:20151205135553p:plain

f:id:tac7:20151205135728p:plain

※トリガーの編集でバッチ起動の時間をシステム起動時間から遅らせることができる遅延時間を設定することができます。システムの起動時にPDBの起動を行うとOracleDBが起動していない状態でPDBの起動処理を実行してしまうことがあります。遅延時間に数分を設定することによりOracleDB起動後にPDBの起動処理を行うことができます。もし、上記の設定でPDBが起動しないときは遅延時間を調整してみてください。


まとめ

Oracle12Cで新しく登場したPDBですが、PCやサーバーの再起動時に自動で起動するためには、ご使用のバージョンによって今回説明させていただいた内容を参考にしていただければと思います。

ネットワークの基礎(全4回) 第1回 ネットワークの環境及び接続形態

はじめまして、ゴルフ場基幹システム開発担当している茨城出身のかたちんです。
書かせて頂く内容としては、インフラ及びセキュリティ関連になります。
初回はネットワークにおける環境や接続形態を書きます。

目次


  • ネットワークの基本環境
  • ネットワークの接続形態
    • スター型(ツリー型)
    • バス型
    • リング型

ネットワークの基本環境


企業や自宅などで利用されるそれぞれのネットワーク環境を説明します。

スタンドアローン

コンピュータを単体で利用している状態で、昔はファイルをFDでやり取りしたり、スイッチ切替て印刷したり面倒な作業を行っていました。
現在ではあまり利用するケースが少なくなってきましたが、外部と直接接続がされていない為、セキュリティ面を考慮した利用ケースが存在します。
f:id:katachin7788:20151130124040j:plain

LAN(Local Area Network)

同じ施設内またはフロア内の限定された狭い範囲内でのネットワークで、コンピュータやプリンタなどの資源をLANケーブルや無線と接続して共有できる環境です。
LANケーブルを集積するHUBやスイッチなどの集線装置を利用します。
HUBやスイッチの違いについては次回以降で説明致します。
f:id:katachin7788:20151130124132j:plain

WAN(Wide Area Network)

遠隔地にあるLAN環境同士を結ぶネットワーク環境で、専用回線や電話回線などを利用して接続します。
LANとWANの違いとしては、通信事業者が提供するネットワークサーブスを利用するかになります。
f:id:katachin7788:20151130124141j:plain

インターネット

世界中の不特定多数の人たちが接続して利用することができるネットワーク環境で、主にホームページの閲覧やメールのやり取りで使われます。 インターネット環境もWAN環境同様、LAN環境から外部に接続して資源を共有することが出来るという点では共通していますが、それを利用するための約束事に違いがあります。
WANは限定された環境に対してのみ利用することしかできませんが、一方のインターネットは公開された場所であれば、誰でも利用することが可能です。
f:id:katachin7788:20151130124148j:plain

DMZ(DeMilitarized Zone)

武装帯とも言われ、インターネットなどの外部環境に公開するための環境です。
外部公開用のコンピュータを内部に設置した場合、内部ネットワークに接続されたコンピュータへの不正なアクセスなどの危険があるため、 中間にファイアウォールを設置し、外部からはDMZ環境にしか接続はできないようにアクセス制御を行います。
f:id:katachin7788:20151130124155j:plain

ネットワークの接続形態


ネットワークを構築する際の各コンピュータなどとの接続内容についてです。

スター型(ツリー型)

HUBやスイッチなどの中継装置を中央に配置し、 各コンピュータやプリンタなどにLANケーブルを接続して利用するネットワーク接続形態。
f:id:katachin7788:20151203205506j:plain

バス型

1本の同軸ケーブルを利用するネットワーク接続形態で、両端に終端装置(ターミネータ)を設置します。
各中継アダプタ(トランシーバ)を設置し、各コンピュータやHUB(スイッチなど)に接続します。
f:id:katachin7788:20151203205512j:plain

リング型

リング状のネットワークに各コンピュータなどを接続した形態で、トークリングや光ファイバを利用したFDDIがあります。
リング状の中をトークンと呼ばれる箱のようなものが巡回し、送受信するデータがあればトークンに詰めて運びます。
論理的にみるとリング状に見えますが、物理的にみるとスター型のように中央に集線装置(コンセントレータ)を設置し、各コンピュータに接続しています。
f:id:katachin7788:20151203205516j:plain

まとめ


自分自身、独学でネットワークやらセキュリティなどを勉強し、 買った本が大雑把に説明され、最初は何を言っているのかさっぱりわからず何度も挫折しそうになりましたが、 折角やり始めたことなので諦めずに色々な書物を読み漁りました。
なのでなるべくわかり易く心掛けて今後も書いていきますので宜しくお願い致します。

VisualStudioチームエクスプローラーでのGit設定

井上です。
Visual StudioでのGit設定についての備忘録です。
この手のものは最初しかやらないだけに、次回やるときに忘れていることが多いので。

環境

GitBucket 2.6
Visual Studio Pro 2013

自身で作成したソリューションをリモートリポジトリに紐付ける

GitBucketの設定

リポジトリを普通に作るのみです*1

f:id:ihisa:20151210171334p:plain

Visual Studioの設定
  1. 対象ソリューションをVSで開く
  2. ソリューション右クリックでソリューションをソース管理に追加→gitを選ぶ*2
    f:id:ihisa:20151210161336p:plain
  3. ローカルリポジトリを作らされるので作る*3
    f:id:ihisa:20151210200233p:plain
  4. ロカールリポジトリにコミットする
    f:id:ihisa:20151210173744p:plain
  5. そのまま同期を押下し、リモートリポジトリに発行する。ここでGitBucketの対象リポジトリのHTTP Clone URLを入れる。
    f:id:ihisa:20151210173810p:plain

既に作成済みのソリューションをGitリポジトリから取得する

新規作成
  1. チームエクスプローラのローカルGitリポジトリから複製を選択
  2. GitBucketの対象リポジトリのHTTP Clone URLおよびローカルリポジトリディレクトリを入力し複製押下
    f:id:ihisa:20151210172109p:plain
  3. 作成完了後、ローカルGitリポジトリから作成したリポジトリを選択 f:id:ihisa:20151210201024p:plain
  4. チームエクスプローラーのホームアイコンを押下
  5. ソリューションに複製したソリューションがでている事を確認。これをダブルクリックすればソリューションが開く。 f:id:ihisa:20151210172334p:plain
ブランチを取得
  1. チームエクスプローラーのホームアイコンを押下
  2. 分岐を押下
  3. 新しい分岐を押下するとドロップダウンでブランチが表示されるので取得したいブランチを選択
    f:id:ihisa:20151210172452p:plain
  4. 分岐の作成を押下
    f:id:ihisa:20151210172513p:plain
  5. 発行された分岐を選択すればそのブランチに切り替わる
    f:id:ihisa:20151210172551p:plain

*1:initialize this repository with a READMEにチェックを入れてリポジトリを作成すると、Visual Studioから[ローカル分岐masterをリモートリポジトリoriginに発行できません。同じ名前の分岐が既に存在します。ローカル分岐の名前を変更してもう一度やり直してください]とメッセージ表示され同期できませんので注意

*2:既に別ソリューションをgit連携していたり、Visual Studio Onlineの設定が無い場合は無条件でgitになるようです

*3:既に別ソリューションでgit連携していたりすると、自動的にローカルリポジトリができるようです(実際になりました)

PowerShellでDynamoDBにJSONドキュメントを格納する

こんにちは、AWS担当のwakです。

さて最近はMongoDBとお友達になりつつあったのですが、「AWS担当の」と言いつつDynamoDBを全く触っていないというのは良くないのではないかと思い始めました。皆様ご存知の通り、DynamoDBはAWSの提供するNoSQLデータベースサービスで、

  • フルマネージド:難しいことはAWSが引き受けてくれる
  • 安定性:膨大な量のサイズのデータを格納しても自動的にシャーディングが行われ(しかもオンラインで)、レイテンシは変わらない
  • JSONドキュメントをそのまま格納できる
  • 高可用性:自動的に3拠点間で多重化される

といった特徴を持つ、まさにクラウドの申し子のような代物です。また個人的には無料利用枠が割と大きめなのが重要な点で、巨大というには程遠く、手で管理するには大きすぎるJSONデータをカジュアルに管理するため、MongoDBの代わりに利用する選択肢になり得ます。今回はPowerShellでこのDynamoDBを触る方法について説明します。

f:id:nurenezumi:20151204113226j:plain

猫キャッチャー

サンプルコード

まず最初に完全な形のサンプルコードを示します。

Add-Type -Path "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSSDK.DynamoDBv2.dll"

$accessKey = "********************";
$secretAccessKey = "****************************************";
$endPoint = [Amazon.RegionEndpoint]::APNortheast1;

$client = New-Object Amazon.DynamoDBv2.AmazonDynamoDBClient($accessKey, $secretAccessKey, $endPoint);
$tableName = "YOUR-TABLE-NAME" # 自分で作ったテーブル名
$table = [Amazon.DynamoDBv2.DocumentModel.Table]::LoadTable($client, $tableName);

$document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson('{...}') # 引数はJSON形式のテキスト
$table.PutItem($document)

前提条件は以下の通りです。

  • DynamoDBでテーブルを作成してあること
  • IAMユーザーを作成し、適切な権限が割り当ててあること
  • IAMユーザーのAPIキーを作成してあること
  • AWS Tools for Windows PowerShellがインストール済みであること
  • PowerShell 3.0以上

以下はこのコードの説明です。

DLLを直接読み込めば大抵のことはできる

上述の AWS Tools for Windows PowerShell をインストールすると、自動的に環境変数PSModulePath*1SDKのパスが追加され、特に何かを意識しなくてもGet-EC2InstanceStart-EC2InstanceといったAWSのためのコマンドレットが使えるようになります。しかしリファレンスを参照すると分かるように、現時点では項目の追加・削除・編集のためのコマンドレットは用意されていません。そのためには(上のコードのように)直接SDKの中のコードを呼び出す必要があります。

そんなときはAdd-Typeで直接DLLを読み込んでしまいましょう。

PS C:\> [Amazon.RegionEndPoint]::APNortheast1
型 [Amazon.RegionEndPoint] が見つかりません。この型を含むアセンブリが読み込まれていることを確認してください。
発生場所 行:1 文字:1
+ [Amazon.RegionEndPoint]::APNortheast1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (Amazon.RegionEndPoint:TypeName) []、RuntimeException
    + FullyQualifiedErrorId : TypeNotFound

PS C:\> Add-Type -Path "C:\Program Files (x86)\AWS Tools\PowerShell\AWSPowerShell\AWSSDK.Core.dll"
PS C:\> [Amazon.RegionEndPoint]::APNortheast1

SystemName                                                  DisplayName
----------                                                  -----------
ap-northeast-1                                              Asia Pacific (Tokyo)

このようにアセンブリが直接ロードできて使えるようになります。

DynamoDBの準備をする

まずDynamoDBのテーブルを作成します。パーティーションキー、ソートキーを適当に設定しました。なお、このキーは1度作ったらもう変更できません(変えたければテーブルを作り直して移行する必要があります)。

f:id:nurenezumi:20151204102329p:plain

これができたら、このテーブルを触る権限を持ったIAMユーザーを準備し、APIキーを取得しておきます。

レコードを登録・更新・削除する

新規登録

まず空のテーブルへレコードを実際に登録してみます。レコードをまずPowerShell連想配列@{Key1=値1; Key2=値2; ...}で作成し、JSON形式のテキストを介してAmazon.DynamoDBv2.DocumentModel.DynamoDBEntryクラスのインスタンスに変換してからDynamoDBへ送信しています。

# 投入するデータ
$source = @(
 @{name = "Tama"; timestamp = "2015-12-04T00:00:00.000Z"; cuteness = 90;  mood = "暇"},
 @{name = "Tama"; timestamp = "2015-12-04T01:00:00.000Z"; cuteness = 100; mood = "とても暇"},
 @{name = "Tama"; timestamp = "2015-12-04T02:00:00.000Z"; cuteness = "INF"; mood = "睡眠"},
 @{name = "Wak";  timestamp = "2015-12-04T00:00:00.000Z"; cuteness = 0;   mood = "腹減った"; memo = "ラーメン" })

$source | % {
  $json = $_ | ConvertTo-Json -Compress
  $document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson($json)
  $table.PutItem($document)
}

登録結果はManagement Consoleから確認することができます。cutenessの型は数値だったり文字列だったり、memoはあったりなかったりしますが、それでもDynamoDBは柔軟に受け入れてくれています。

f:id:nurenezumi:20151204104059p:plain

更新する

テーブルを作るときに決めたキー(ここではnametimestamp)が両方とも完全に一致するレコードを登録すると更新になります。つまりPutItemSQLのINSERT文ではなくMERGE/UPSERT文に相当するわけです。したがって次のコードはデータ部分以外は上と同じです。memo入れ子にした連想配列をセットしてみました。

$source = @(
 @{name = "Tama"; timestamp = "2015-12-04T02:00:00.000Z"; cuteness = "INF"; mood = "睡眠"; memo = "あおむけ"}, # 更新
 @{name = "Tama"; timestamp = "2015-12-04T03:00:00.000Z"; cuteness = 100; mood = "ごはん"; memo = @{menu = "高級かりかり"; ammount = 2 }})

$source | % {
  $json = $_ | ConvertTo-Json -Compress
  $document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson($json)
  $table.PutItem($document)
}
f:id:nurenezumi:20151204134009p:plain

1行は更新、1行は新規追加になりました。memoカラムにはJSONが生で書いてあるように見えますが、よく見ると"N""S"といった見覚えのないキーが出てきています。これはDynamoDBが自動的に与えた型情報です。こちらもきちんと入れ子構造を持ったドキュメントとして管理されている証だと思ってください。

削除

ついでに削除も試しておきます。こちらも構文はまったく同じで、とにかくキーを指定してレコードを特定できればそれが処理対象になると考えればいいです。

$removed = @{name = "Wak"; timestamp = "2015-12-04T00:00:00.000Z"} | ConvertTo-JSON -Compress
$document = [Amazon.DynamoDBv2.DocumentModel.Document]::FromJson($removed)
$table.DeleteItem($document)
f:id:nurenezumi:20151204142233p:plain

レコードを検索する

次は検索を行います。検索に相当するコマンドレットも用意されていないので、やはりこちらもSDKを直接呼び出します。たとえば SELECT * FROM helloDynamoDB WHERE cuteness > 80 に相当するスキャン*2なら、カラム名演算子、値を順に指定して

$scanFilter = New-Object Amazon.DynamoDBv2.DocumentModel.ScanFilter
$scanFilter.AddCondition("cuteness", [Amazon.DynamoDBv2.DocumentModel.ScanOperator]::GreaterThan, 80)
$search = $table.Scan($scanFilter)

で検索ができます。この時点で得られる$searchはカーソルのようなもので、実際の結果はGetNextSet()を呼び出して取得します(カーソルとは違って1件ずつではなく、最大1MB分のデータがまとめて返されます)。

PS C:\> $search = $table.Scan($scanFilter)
PS C:\> $documentList = $search.GetNextSet()
PS C:\> $documentList

Key                                                         Value
---                                                         -----
timestamp                                                   2015-12-04T00:00:00.000Z
mood                                                        暇
name                                                        Tama
cuteness                                                    90
timestamp                                                   2015-12-04T01:00:00.000Z
mood                                                        とても暇
name                                                        Tama
cuteness                                                    100

確かに2件の結果が取得できました。

実際の用途

今回はPowerShellからDynamoDBを利用する方法について書きました。次回はAWS ConfigのログをDynamoDBに格納し、PowerShellから検索して変更履歴を出力するという(多少は)実用的な用途に使ってみたいと思います。お楽しみに!

*1:ここで指定されたパスに配置されたモジュールは自動的に読み込まれるようになります。環境変数PATHのPowerShell版だと思えばだいたい合っています

*2:インデックスを使わないテーブルスキャンのようなものだと考えてください。したがって低速ですがすべての項目が検索対象に指定できます。テーブル作成時に指定したnameを使う検索なら、「スキャン」ではなく高速な「クエリ」が利用できます

C#でEF+LINQを用いてMySQLを使用するメリット・デメリット

井上です。
SQL Server同様、MySQLでもEntity FrameworkとLINQを用いて当然やりたいですよね。

環境

.NET Framework 4.5.1
MySQL Entity Framework 6
MySQL 5.6
C#

Entity Framework・LINQを用いるメリット

何故Entity Frameworkを使いたいかとなると、以下の様なメリットがあるからです。

データベースの定義変更に強い

DBの定義更新があった場合、モデル(edmxファイル)を更新することでプログラムに即反映できます。
DataSet等だとこれは出来ないので大きなメリットだと思います。

とにかく開発効率が良い

実際書いてみるとわかりますが1行でSELECTでき、且つ取得結果を好きな形で返せるというのは大きいです。 DBという意識無く書けるのはすごいですよね。

Entity Framework・LINQを用いたことによるデメリット

想定と異なるSQLが生成される

MySQLだからというよりEntity Frameworkによるところも多いかと思いますが、メソッドチェーンでJoin/GroupJoinを複数行うような場合、パフォーマンスが出ず致命的になる場合もあります。

遅延実行に注意しないといけない

実際にSQLが実行されるタイミングは結果が必要になるとき(Count(),ToList(), ToArray()等)なので、そのタイミングを考えてコードを書く必要があります。

LINQで日付条件比較が一致しない

MySQLのdate型はC#のDateTime型にマッピングされています。

DateTime target= new DateTime(2015, 12, 01);
var result = db.TableA.Where(x => x.date_column == target).ToList();

当然こんな風に書きたいところですが、残念ながらdate_columnカラム値が2015-12-01の値となっているレコードは取得されません。
1ミリ秒ずらすという苦肉の策で期待通りの結果は得られるようになりました。

DateTime from= new DateTime(2015, 12, 01);
DateTime to = searchDate.AddMilliseconds(-1);
var result = db.TableA.Where(x => x.date_column >= from && x.date_column <= to).ToList();

尚、SqlQueryで直接SQLParameter指定した場合は普通に期待通りの結果がかえってきます。

using (var db = new hogehogeEntities())
{
    string sql = "SELECT hogehoge FROM TableA WHERE date_column = @targetdate";

    List<MySql.Data.MySqlClient.MySqlParameter> parameters = new List<MySql.Data.MySqlClient.MySqlParameter>();
    parameters.Add(new MySql.Data.MySqlClient.MySqlParameter("@targetdate", MySql.Data.MySqlClient.MySqlDbType.Date));
    parameters[0].Value = targetdate;

    var result = db.Database.SqlQuery<クラス>(sql, parameters.ToArray()).ToList();
}

そもそものMySQLC#間の問題

Entity FrameworkおよびLINQとは関係ありませんが。

日付の初期値

MySQLのDateTimeの最小値は[0000-00-00 00:00:00]ですが、C#のDateTime型は最小値が[0001-01-01 00:00:00]です。
このためそのままMySQLの値を代入すると例外が発生します。
DB接続文字列にConvert Zero Datetime=true を追加することで代入不可な日付値をMinValueに自動的に変換してくれます。

BLOB型を追加・更新できない

こちらに記載しています。
C#EF6からMySQL-BLOBにデータを登録 - Sanwa Systems Tech Blog

まとめ

個人的にはラップすると内部で何やってるのか見えなくなってしまうので、SQLは直に書きたいところですが、LINQは時と場合で非常に強力なのは事実・・・。
多くのJOINを重ねたりするととってもボリューミーなSQLが自動生成されてしまったりするのでどこまでLINQでやるべきなのか等チーム内で決めるのが大事かと思います。
複雑なSQL書くなよって話もあるかと思いますが、回避できないときも多々あるかと思いますので。

以下ブログも参考いただければ幸いです。

Entity Framework と Linq を使いこなしたい! - Sanwa Systems Tech Blog LINQ(メソッド構文)を用いた複雑めなSQL発行時のメモ - Sanwa Systems Tech Blog

CrystalReportsの印字位置をC#側で直接指定する

はじめまして。ゴルフ場基幹システム開発リラックマ担当のむとうです。
ほぼ生粋の茨城出身です。業務では主にC#SQLServerを触っております。どうぞ宜しくお願いします。


今回のテーマ

弊社の基幹システムでは帳票出力にCrystalReportsを使用しています。
一般的に、レポート内フィールドの「表示/非表示」「フォント指定」などの書式設定を条件によって変える場合、「式エディタ」で制御すると思います。 しかし「フィールドの印字位置」を条件によって変えようとした場合、どのように制御すれば良いのでしょうか。

例えば、

  • 印字位置指定が想定される全ての場所に同じフィールドを配備、非表示制御で制御…

この方法は印字指定が数パターンで済めば力技でできなくもないのですが、ミリ単位でパターンを指定しようなどと考えた日には気が遠くなります。

…と、このような事を悩んだ経緯から、今回は「印字位置を出力元(C#)で直接指定する」方法を書きたいと思います。

環境

.NET Framework 4
C#
Crystal Reports for Visual Studio 2010

下準備

  • 「レポート出力を行う親フォーム」と「プレビュー表示を行う子フォーム」を作成。
  • ハガキ印刷を想定したレポートテンプレートを作成。 f:id:muto70:20151124164504j:plain
    • サンプルなので今回はテキストオブジェクトの直貼りです。
    • 各フィールドは上から[郵便番号][住所1][住所2][氏名]の4つとなります。

動作確認

まずはこのままプレビューを行います。テンプレートのままですね。 f:id:muto70:20151121182113j:plain

実際に印字位置を指定する

親フォーム側にコードを書き足します。

  • フィールド名を指定して位置を動かしてみます。
using (ReportDocument report = new ReportDocument())
    {
        //レポートテンプレート読込
        string path = "C:\\tryProject\\try.rpt";
        report.Load(path);

        // 氏名フィールドを2センチ程右下へ移動
        report.ReportDefinition.ReportObjects["氏名"].Top += 1200;
        report.ReportDefinition.ReportObjects["氏名"].Left += 1200;

        //(以下レポートプレビュー処理)

プレビュー画面。氏名欄のみ右下に移動しました。 f:id:muto70:20151121183846j:plain

  • 全フィールドを纏めて指定して位置を動かしてみます。
using (ReportDocument report = new ReportDocument())
    {
        //レポートテンプレート
        string path = "C:\\tryProject\\try.rpt";
        report.Load(path);

        // 全フィールドを2センチ程右下へ移動
        foreach (ReportObject r in report.ReportDefinition.ReportObjects)
        {
            r.Top += 1200;
            r.Left += 1200;
        }

        //(以下レポートプレビュー処理)

プレビュー画面。全体が右下に移動しました。 f:id:muto70:20151121183847j:plain

良い具合です!


まとめ

今回のサンプルでは「ハガキ用紙枠への印字被り対応」を想定しています。 例えばプリンタの買替などで環境が変わると印字位置も微妙に変わってしまうケースがあるのですが、出力元で印字位置を指定できるようになればエンドユーザー側で印字位置調整が行う事も不可能ではないのです。 今回はプログラム上で印字位置を直接設定していますが、UI側での値指定や外部設定ファイル参照を行えるようにすることで、より柔軟に対応できるようになります。

C#で他アプリケーションを操作するための基礎知識

こんにちは、大昔はVC++Windowsプログラミングをしていたwakです。先日業務でC#のコードから他のWindowsアプリを強制的にコントロール(メニューをクリックしたり、キー操作を行ったりといった手動操作をエミュレートしてアプリを制御する)して処理を自動化する必要に迫られ、頑張ってそんな感じのコードを書きました。今日はそのための基礎知識とサンプルコードをご紹介します。

もくじ

  • Windowsの仕組み
  • ウィンドウハンドルとメッセージ
  • メッセージの内容
  • 電卓を例にしたサンプル
f:id:nurenezumi:20151125110735j:plain

猫は操作できません

Windowsの仕組み……Windowsはウィンドウでできている

まずWindowsでは、

  • ウィンドウ(の外枠)
  • ボタン
  • テキストボックス
  • ラベル
  • デスクトップ
  • タスクバー

といった要素はすべて「ウィンドウ」です。要するに「ウィンドウ」という基底クラスを継承したオブジェクトが様々な外見や機能を持っているだけだと考えましょう。たとえばこれはWindows標準の電卓ですが、赤枠で囲ったものはすべて「ウィンドウ」です。

f:id:nurenezumi:20151120125142p:plain

パネル、ボタンやラベルまでが皆ウィンドウであることが分かるかと思います。

もちろん全部が全部「ウィンドウ」というわけではありません。タイトルバーやメニューは親のウィンドウの一部なので独立した「ウィンドウ」ではありません。Webページに表示されているボタンやテキストボックスはブラウザが描いている絵でしかありませんから「ウィンドウ」ではありません。シューティングゲームの弾や敵キャラが、(ゲーム内部ではオブジェクトかもしれませんが)「ウィンドウ」ではないのと同じです。

ウィンドウハンドルとメッセージ

さて、すべての「ウィンドウ」にはWindowsから一意のIDが振られます。これは32ビットの符号なし整数で、「ウィンドウハンドル」と呼ばれます。一部の値(さっき試したときの実際の値です)を書いてみました。もちろんこの値は電卓を複数起動したり再起動したりすると毎回変わります。

f:id:nurenezumi:20151120125202p:plain

このウィンドウハンドルは、Windowsアプリケーションが動作するにあたってとても重要な値です。たとえばユーザーが「5」ボタンをクリックしたとしましょう。すると、calc.exe(電卓)はWindowsから以下のような内容のメッセージを受け取ります。*1

  • メッセージのタイプは「マウスの左ボタンが押された(WM_LBUTTONDOWN)」だよ
  • 対象のボタンのウィンドウハンドルは 0x00022012 だよ
  • 一緒に押されていたキー(マウスの右ボタン、キーボードのSHIFTキーなど)はなかったよ
  • クリックされた場所の座標は(8, 11)だよ

メッセージを受け取ったプログラムはこれを解釈し、「Windowsから伝えられたメッセージはWM_LBUTTONDOWN、ウィンドウハンドルは0x00022012だったな。つまり"5"のボタン*2がクリックされたわけだから、それに応じた処理をしよう……」と考えるわけです。普段C#でプログラムを書いている人はこんなことを意識しませんが、ここら辺は .NET Framework がよろしくやって隠蔽してくれています。

メッセージは、作れる!

通常、「マウスがクリックされた」「キーが押された」といったメッセージはユーザーの操作に応じてWindowsがプログラムに対して発行されるものです。しかし、メッセージは自分で他のプログラムへ送信することもできるのです。これを使えば、自分のプログラムから他のアプリケーションの画面を「クリックしたことにする」「キー操作したことにする」といったこともできるようになります。これが冒頭で触れた「他アプリのコントロール」「自動操作」だということになります。

メッセージの内容

メッセージには4つのパラメータが含まれています。他のウィンドウにメッセージを送る関数の一つ、SendMessage関数の宣言をMSDNから引用してみましょう。

LRESULT SendMessage(
  HWND hWnd,      // 送信先ウィンドウのハンドル
  UINT Msg,       // メッセージ
  WPARAM wParam,  // メッセージの最初のパラメータ
  LPARAM lParam   // メッセージの 2 番目のパラメータ
);

これはC++のアンマネージドコードなので、見慣れない型がたくさん並んでいて逃げ出したくなるかもしれません。型については

を参考にして、さらに必要な宣言を加えてC#に書き換えるとこうなります。 *3

[DllImport("user32.dll")]
extern long SendMessage(
  IntPtr hWnd,    // 送信先ウィンドウのハンドル
  uint Msg,       // メッセージ
  uint wParam,    // メッセージの最初のパラメータ
  uint lParam     // メッセージの 2 番目のパラメータ
);

だいぶ親しみが持てる形になりました。4つの引数を順に説明してゆきます。

1. ハンドル

これは最初に説明しました。ハンドルが0x00022012であれば、new IntPtr(0x00022012)のように書けます。既存のウィンドウからウィンドウハンドルを取得する方法については後述します。

2. メッセージ

Windowsには様々なメッセージが定義されています。ここにはそのメッセージを示す整数を渡します。よく使いそうなものを挙げるとこんな感じです。

名前 意味
WM_KEYDOWNキーボードのキーが押された0x0100
WM_KEYUPキーボードのキーが離された0x0101
WM_LBUTTONDOWNマウスのボタンが押された0x0201
WM_LBUTTONUPマウスのボタンが離された0x0202

完全な一覧はAbout Messages and Message Queues (Windows)にあります。マウス・キー操作はWM系を参照すれば良いでしょう。C++であればwinuser.hというヘッダファイルをインクルードすれば一挙に定数がインポートできるのですが、C#にはそのようなものはないので自前で定数を定義して使うことになります(別にハードコードしても構いませんが)。

なお、受け取ったメッセージをどのように処理するかはアプリケーションの自由です。また、メッセージとはいえども実体はただの整数値ですから、アプリケーション単位で好きな値を決めて独自のメッセージを定義することもできます。同時起動した同じアプリ同士、あるいは異なるアプリ同士*4で連携を行う場合などに使われています。詳しくはWM_USERで検索してみてください。

3~4. パラメータ

メッセージの内容をさらに詳細に示すためのパラメータです。「最初の」とか「2番目の」とか曖昧な表現になっているのは、メッセージの種別によって担うべき情報が異なるからです。たとえばWM_LBUTTONDOWNでは、

  • 最初のパラメータ……同時に押された他のキーやボタンを示す
  • 2番目のパラーメータ……クリックされた場所の座標を示す(下位2バイトがX座標、上位2バイトがY座標)

となっています。またWM_KEYDOWNでは、

  • 最初のパラメータ……押されたキーの仮想キーコードを示す
  • 2番目のパラーメータ……オートリピート中であるかどうかなどの補助情報

が格納されます。一部のメッセージでは常に固定値となっているものもあります。こういった情報はメッセージ名で検索すると容易に見つけることができます。

電卓を例にしたサンプル

ここまでの話を具体的に試すためのサンプルとして、起動中の電卓の「5」ボタンを押すコードを書いてみます。こんな手順になります。

  1. 電卓のウィンドウハンドルを検索する(これで親となるウィンドウのウィンドウハンドルが分かる)
  2. ウィンドウの親子構造をたどり(あるいは再帰的に子ウィンドウを探し)目的のボタンを見つける
  3. 見つけたボタンのウィンドウハンドルにWM_LBUTTONDOWNを送る(これでマウスのボタンを押したことになる)
  4. 見つけたボタンのウィンドウハンドルにWM_LBUTTONUPを送る(これでマウスのボタンを離した=クリックが完了したことになる)

目当てのウィンドウを見つけるのはWebでスクレイピングを行うのと似ています。ラベルとクラスだけで特定できれば楽なのですが、そうはいかないケースも多々あります。たとえば電卓の場合、ボタンのラベルは「文字」ではなく「絵」(グラフィック)で描画されていますので、ラベルは全てのボタンでブランクになっています。今回は強引に「全てのボタンを列挙し、その10番目を選ぶ」という方法で行ってみます。

ウィンドウの探し方

WindowsAPIFindWindowEx関数などを使って探すのですが、冗長になるためこの記事の最後に完全なコードを示します。今はあらかじめ分かっていることにします。

サンプルコード

class Program
{
    public const int WM_LBUTTONDOWN = 0x201;
    public const int WM_LBUTTONUP = 0x202;
    public const int MK_LBUTTON = 0x0001;

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);

    static void Main(string[] args)
    {
        // 電卓のトップウィンドウのウィンドウハンドル(※見つかることを前提としている)
        var mainWindowHandle = Process.GetProcessesByName("calc")[0].MainWindowHandle;

        // 対象のボタンを探す(これでボタンのハンドルが取得できる)
        var hWnd = FindTargetButton(mainWindowHandle);

        // マウスを押してから放す
        SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0x000A000A);
        SendMessage(hWnd, WM_LBUTTONUP, 0x00000000, 0x000A000A);
    }

    public static IntPtr FindTargetButton(IntPtr hTopWindow) { /* ... */ }
}

まとめ

というわけで、全体の流れを復習するとこんな感じです。

【事前準備】

  • 対象のアプリを事前に手動操作してどんなメッセージを送るべきかを確認しておく
  • 対象のアプリのウィンドウ構造を確認しておく
  • 対象のアプリの操作対象となるウィンドウをどのように探せば良いかを確認しておく

【プログラム側】

  • 対象のアプリのウィンドウを検索してウィンドウハンドルを取得する
  • 決まった順にメッセージを飛ばしてマウスやキー操作をエミュレートする(必要があれば適宜ウェイトを入れる)

Windowsのベースとなる仕組みを使っているため、非常に煩雑ではありますが、何か特殊な対策を行っていない限り原理的にはどのようなアプリケーションも制御できることになります。使えるものは全部使って良い自動化ライフを送りましょう。それでは。

完全なコード

class Program
{
    public const int WM_LBUTTONDOWN = 0x201;
    public const int WM_LBUTTONUP = 0x202;
    public const int MK_LBUTTON = 0x0001;
    public static int GWL_STYLE = -16;

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);
    
    [DllImport("user32.dll")]
    public static extern IntPtr FindWindowEx(IntPtr hWnd, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
    
    [DllImport("user32")]
    public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
    
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int GetWindowTextLength(IntPtr hWnd);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);


    static void Main(string[] args)
    {
        // 電卓のトップウィンドウのウィンドウハンドル(※見つかることを前提としている)
        var mainWindowHandle = Process.GetProcessesByName("calc")[0].MainWindowHandle;

        // 対象のボタンを探す
        var hWnd = FindTargetButton(GetWindow(mainWindowHandle));

        // マウスを押して放す
        SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0x000A000A);
        SendMessage(hWnd, WM_LBUTTONUP, 0x00000000, 0x000A000A);
    }

    // 全てのボタンを列挙し、その10番目のボタンのウィンドウハンドルを返す
    public static IntPtr FindTargetButton(Window top)
    {
        var all = GetAllChildWindows(top, new List<Window>());
        return all.Where(x => x.ClassName == "Button").Skip(9).First().hWnd;
    }


    // 指定したウィンドウの全ての子孫ウィンドウを取得し、リストに追加する
    public static List<Window> GetAllChildWindows(Window parent, List<Window> dest)
    {
        dest.Add(parent);
        EnumChildWindows(parent.hWnd).ToList().ForEach(x => GetAllChildWindows(x, dest));
        return dest;
    }

    // 与えた親ウィンドウの直下にある子ウィンドウを列挙する(孫ウィンドウは見つけてくれない)
    public static IEnumerable<Window> EnumChildWindows(IntPtr hParentWindow)
    {
        IntPtr hWnd = IntPtr.Zero;
        while ((hWnd = FindWindowEx(hParentWindow, hWnd, null, null)) != IntPtr.Zero) { yield return GetWindow(hWnd); }
    }

    // ウィンドウハンドルを渡すと、ウィンドウテキスト(ラベルなど)、クラス、スタイルを取得してWindowsクラスに格納して返す
    public static Window GetWindow(IntPtr hWnd)
    {
        int textLen = GetWindowTextLength(hWnd);
        string windowText = null;
        if (0 < textLen)
        {
            //ウィンドウのタイトルを取得する
            StringBuilder windowTextBuffer = new StringBuilder(textLen + 1);
            GetWindowText(hWnd, windowTextBuffer, windowTextBuffer.Capacity);
            windowText = windowTextBuffer.ToString();
        }

        //ウィンドウのクラス名を取得する
        StringBuilder classNameBuffer = new StringBuilder(256);
        GetClassName(hWnd, classNameBuffer, classNameBuffer.Capacity);

        // スタイルを取得する
        int style = GetWindowLong(hWnd, GWL_STYLE);
        return new Window() { hWnd = hWnd, Title = windowText, ClassName = classNameBuffer.ToString(), Style = style };
    }
}

class Window
{
    public string ClassName;
    public string Title;
    public IntPtr hWnd;
    public int Style;
}

*1:この一瞬後、プログラムはさらに「マウスの左ボタンが離されたよ」というメッセージも受け取ることになりますが、話が面倒になるので省略します

*2:どのウィンドウハンドルがどのボタンを示すかの対応表はプログラムが自分で覚えておく必要があります

*3:最後のlParamは本当はint型になるはずですが、パラメーターを作るのが面倒なのでuint型にしています。どのみちビット列の解釈の違いでしかないので問題にはなりません

*4:つまりプロセスが異なるのでメモリも共有できない状態