愚者の経験

「また今度」はほとんどこない

月別アーカイブ: 3月 2012

Access2010のマクロの困った変更

ずばり「サブマクロ」です。
なんと内容が空のサブマクロは動かなくなっています。

このためAccessの2010で今まで通り「Autokeys」マクロを作成すると
ファンクションキーが無効になりません。

Access2007(以前でもいいか?)からインポートしていれば正常に動きますが
編集するとサブマクロの内容がない場合やはり無効になります。

何か設定する必要がありますが、無難なのは「マクロエラーのクリア」でしょうか…

広告

Access 自分自身を最適化

Access2007以降になって自分自身を最適化するのは難しくなっています。

そこで同じディレクトリにVBScriptを作って、パスを渡して最適化させるものを作りました。
Compact.vbs

Option Explicit
‘***注意***
‘他にAccessプログラムが起動してあると失敗します。
Dim Shell
Dim accdb

‘Accessを終了するまでの時間稼ぎ
MsgBox “最適化を開始します。”, vbInformation, “最適化開始”

Set Shell = CreateObject(“WScript.shell”)

‘Accessファイルパスを引数で渡して最適化を実行する
For Each accdb In WScript.Arguments
Shell.Run “cmd /c ” & accdb & ” /compact”, 0
WScript.Sleep 1000
Next

MsgBox “完了しました。”, vbInformation, “最適化完了”

‘最初の引数に指定したAccessファイルを起動する
If WScript.Arguments.Count > 0 Then
Shell.Run “cmd /c ” & WScript.Arguments(0), 0
End If

Set Shell = Nothing

こんな感じでAccessのパスをArgumentsに渡して最適化させます。

複数渡すというのは一応プログラムファイルとデータファイルが分かれていることを想定して、
データとプログラムを同時に最適化するためです。

Access呼び出し側

Public Sub CompactRepair()
On Error GoTo Finally
    Dim sb As String
     
    Beep
    If (MsgBox(“プログラムの最適化を行います。よろしいですか?” & vbCrLf & _
                “※この操作ではプログラムを再起動します。”, vbInformation + vbDefaultButton2 + vbYesNo, “最適化”) = vbYes) Then
        sb = “WScript.exe ” & _
                CurrentProject.Path & “\Compact.vbs ” & _
                String$(2, 34) & CurrentProject.FullName & String$(2, 34) & ” ” & _
                String$(2, 34) & CurrentProject.Path & “\data.accdb” & String$(2, 34)
        Shell sb, vbHide
        Quit
    End If
Finally:
End Sub

呼び出す方法は結構はまりました。
要するに「WScript.exe 」
という感じになります。文字列はダブルクォーテーション2つで括る必要があります。

またVBSの先頭に書いてありますが、他に起動しているAccessプログラムがあると
起動オプション「compact」の挙動が変わり、compactされたファイルが
そのまま起動するのでおかしなことになります。

また最適化前にバックアップを取ることも有効だと思います。

SilverlightのローカルDB「Sterling」を使ってみる

前の投稿でも紹介しました「Sterling」を使ってみます。
C#の少し勉強(と言っても本買ってしたわけでなく実験的にコーディングしただけ)したので
当時わからなかったことでも今は若干理解出来ます。「ラムダ式」はかなり苦手ですが。

参考URL:http://www.slideshare.net/odashinsuke/silverlightwp7-db

やはり「こう書けばできる」より理解できている方が色々応用できます。

まず保存するデータクラス(「エンティティ」というらしい)を定義します。
Accessでいうところのテーブルデザインです。

using System;

namespace Sterling
{
    public class Person
    {
        public Guid ID { get; set; }
        public int Number { get; set; }
        public string Name { get; set; }
    }
}

publicのみ保存対象です。いつもの「Person」クラスです。
「Sterling」はクラスを直接保存するようなデータベースで「RDB」ではなく「OODB」です。

次にデータベースクラスを定義します。
データベースクラスはBaseDatabaseInstance』を継承し「RegisterTables」メソッドをoverrideして
メソッド内で『ITableDefinition』の『List』を返します。

『ITableDefinition』 は「CreateTableDefinition」メソッドで作成します。
「CreateTableDefinition」メソッドの引数に指定したデータクラスを保存できるようになります。

using System;
using System.Collections.Generic;
using Wintellect.Sterling.Database;

namespace Sterling
{
    public class Database : BaseDatabaseInstance
    {
        //インデックス名
        public const string IX_Person_ID = “IX_Person_ID”;
        public const string IX_Person_Number_Name = “IX_Person_Number_Name”;

        protected override List RegisterTables()
        {
            return new List()
            {
                //データクラスを指定(ラムダ式で主キーのプロパティ指定)
                CreateTableDefinition(e=>e.Number)
                    //インデックスを指定
                    .WithIndex
                        //インデックス名,ラムダ式でインデックスの値を返す
                        (IX_Person_ID,e=>e.ID)
                    //インデックスを指定(2個目)
                    .WithIndex
                        //インデックス名,ラムダ式でインデックスの値を返す
                        (IX_Person_Number_Name,e=>new Tuple(e.Number,e.Name))
            };
        }
    }
}

ラムダ式は「パッと書け」と言われると無理です。

インデックスが不要であれば.WithIndex以下の文は要りません。
あとインデックスと言っても「重複なし」だとか指定できるわけはないです。
あくまでメモリ上に持つ値の指定です。※まだ使いたてなのでおそらくですが…

現段階ではGuidはKeyに指定するとエラーになりました。参考URLではKeyに指定できると
書いてあるのでやり方が間違っているかもしれません。

CreateTableDefinition(e=>e.ID)←これで実行時にエラー

また一つのインデックスで指定できる列は2つまでです。データの問い合わせではインデックスは
一つずつしか使用できないので(おそらく)実際に使うとなると考えて作る必要があります。

ここでまた勉強です。『Tuple』クラスは値をひとつにまとめたクラスになります。
最高で8つ指定可能で、Tuple.Item1みたいな形で要素にアクセスします。

実際にこれを使ってみます。
データの追加とDatagridに表示をやっています。
「MainPage.xaml」のコードビハインド

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Wintellect.Sterling;
using Wintellect.Sterling.Database;
using Wintellect.Sterling.IsolatedStorage;

namespace Sterling
{
    public partial class MainPage : UserControl
    {
        private SterlingEngine _engine;
        private ISterlingDatabaseInstance _db;

        public MainPage()
        {
            InitializeComponent();
            _engine = new SterlingEngine();
            _engine.Activate();
            _db = _engine.SterlingDatabase.RegisterDatabase(new IsolatedStorageDriver());
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            //Linqで問い合わせ(インデックスを使用する場合は(インデックス名)
            var grid =from p in _db.Query(Database.IX_Person_Number_Name)

                        //where p.Index.Item1 == 1
                        //インスタンス(実際の値)はLazyValue.Valueにある

                        select p.LazyValue.Value;
            dataGrid1.ItemsSource = grid;
        }

        private void button2_Click(object sender, RoutedEventArgs e)
        {
            //インスタンス追加
            _db.Save(new Person() { ID = Guid.NewGuid(), Number = int.Parse(textBox1.Text), Name = (string)textBox2.Text });
            //保存
            _db.Flush();
        }
    }
}

「MainPage.xaml」

<UserControl x:Class="Sterling.MainPage"
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml&#8221;
    xmlns:d=”http://schemas.microsoft.com/expression/blend/2008&#8243;
    xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006&#8243;
    xmlns:my=”clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data”          
    mc:Ignorable=”d”
    d:DesignHeight=”600″ d:DesignWidth=”800″>
   
       
       
       
       
       
   

Queryメソッドでインデックスを指定するために「インデックスは一つしか使えない」と判断しました。
最高でもKeyとあわせて3列しか指定できないのかもしれません。Tuple入れ子とかできる?

また保存についてはFlashメソッドを呼んでやる必要があります。

これからもいろいろ調べていきます。
もしSterling知っている人がいたらいろいろ教えて欲しいですm(__)m

AccessのZオーダーとタブオーダーと処理順

フォームにサブフォームを複数置いて一つのサブフォームは
「別のサブフォームのカレントレコードの値を参照」とやった時に、「値がありません」的なエラーで
怒られました。

あんまり意識せずにやってましたが、複数のサブフォームはどの順番で読み込まれているか
気になったので調べました。

実験1
フォームにサブフォームを4つ配置し、配置順にフォームの「読み込み時」に

Option Explicit

Private Sub Form_Load()
    Debug.Print
End Sub

でデバッグプリントする。

フォームを開いた結果
1
2
3
4

次はタブオーダーを変更してみる。
今はタブオーダーと配置順は一致しているのでタブオーダーを逆転させてみる。

フォームを開いた結果
1
2
3
4

あれ…一緒でした。タブオーダーは関係無いようです。

次は「最前面へ移動」を適用してみます。

最初に配置したサブフォームを「最前面へ移動」させます。

フォームを開いた結果
2
3
4
1

変わりました。どうやら「後ろにある」オブジェクトから読み込むようです。

こんなこともやってみました。
標準モジュールに以下のファンクション記述

Option Explicit

Public Function Renban(Optional Default As Variant) As Long
    Static v As Long
   
    If (IsMissing(Default) = False) Then
        v = Default
    Else
        v = v + 1
    End If
   
    Renban = v
End Function

テキストボックスを複数配置してのコントロールソースに 「=Renban()」とします。
フォームを開きます。

テキストボックスに連番が表示されます。
「最前面へ移動」を一つずつ適用すると連番をコントロールできます。

「クラスはオートメーションまたは予測したインターフェースをサポートしていません」

「Access2010のService Pack なし」と「Access2010のServise Pack 1」で交互に作ったり
「Windows 7(Windows Server 2008 R2)のService Pack 1」とそれ以外の環境でadpで交互に作ったり
しているとたまにこれが出ます。(要は別の環境で作ったらたまに出ます。)

これが出ると大体コンパイルしても直りません、最適化してもダメです。

一度デコンパイルします。そうすればほとんどOKです。

コードのごちゃごちゃを解消 2

前の投稿の続きです。

さらにクラスを追加します。
「c_EventNotifier」クラス

Option Explicit

Public Event NotifyCommand(Command As String)

Public Sub FireNotifyCommand(Command As String)
    RaiseEvent NotifyCommand(Command)
End Sub

これを使うように「c_得意先」クラスを変更します。
「c_得意先」クラス

Option Explicit

Private Notifier As c_EventNotifier

Public Sub AddNew()
    DoCmd.GoToRecord , , acNewRec
    Notifier.FireNotifyCommand “追加”
End Sub

Public Sub Update()
    DoCmd.RunCommand acCmdSaveRecord
    Notifier.FireNotifyCommand “更新”
End Sub

Public Sub Delete()
    DoCmd.SetWarnings False
    DoCmd.RunCommand acCmdDeleteRecord
    DoCmd.SetWarnings True
    Notifier.FireNotifyCommand “削除”
End Sub

Public Property Set SetNotifier(Value As c_EventNotifier)
    Set Notifier = Value
End Property

これでフォーム側のCommandMessageが不要になります。

「Form_f_得意先」

Option Explicit

Private Model As c_得意先
Private WithEvents Notifier As c_EventNotifier

Private Sub Form_Load()
    Set Notifier = New c_EventNotifier
    Set Model = New c_得意先
    Set Model.SetNotifier = Notifier
End Sub

Private Sub btn更新_Click()
    Model.Update
End Sub

Private Sub btn削除_Click()
    If (MsgBox(“表示中の得意先を削除します。よろしいですか?”, _
                vbInformation + vbYesNo + vbDefaultButton2, “削除確認”) = vbYes) Then
        Model.Delete
    End If
End Sub

Private Sub btn新規_Click()
    Model.AddNew
End Sub

Private Sub Notifier_NotifyCommand(Command As String)
    MsgBox “「” & Command & “」をしました。”
End Sub

処理とイベントで雑多なコードだったのが「c_得意先」クラスに処理が集中することで
見易くなっているかと思います。

処理が複雑になってもフォーム側での呼び出しはほとんど変わりませんし、
「Private Sub(Function)」の山を築かない点でコード量が増えた時にも見やすさを保てます。
また「c_得意先」クラス内ではコントロールを参照できないため、「Me.テキスト.Value」のような
コーディングができなくなり必然的にクラスモジュールの再利用性が高まります。

欠点としてはどこまでをFormモジュールでやるかの線引きが非常に難しいです。
クラスの設計でも結構考えさせられるので、工数がかかります。

コードのごちゃごちゃを解消 1

テキストボックスにコントロールソースを設定して
BeforeUpdate,AfterUpdate,Exit…..いろいろ書いていくとフォームのコード量が
どんどん膨れていきます。作っているときはいいのですが開発の熱が冷めた後で、
改めてコードを見るとごちゃごちゃっぷりに…orz

そうならないために改善案を色々検討してみます。

例として「追加」、「更新」、「削除」の実行時にメッセージボックスを出すコードを書いてみます。
大体ボタンに書くとこんな感じになります。

フォーム「f_得意先」の「Form_f_得意先」Microsoft Accessクラスオブジェクト

Option Explicit

Private Sub btn更新_Click()
    DoCmd.RunCommand acCmdSaveRecord
    CommandMessage “更新”
End Sub

Private Sub btn削除_Click()
    If (MsgBox(“表示中の得意先を削除します。よろしいですか?”, _
                vbInformation + vbYesNo + vbDefaultButton2, “削除確認”) = vbYes) Then

        DoCmd.SetWarnings False
        DoCmd.RunCommand acCmdDeleteRecord
        DoCmd.SetWarnings True

        CommandMessage “削除”
    End If
End Sub

Private Sub btn新規_Click()
    DoCmd.GoToRecord , , acNewRec
    CommandMessage “追加”
End Sub

Private Sub CommandMessage(Command As String)
    MsgBox “「” & Command & “」をしました。”
End Sub

これの処理の部分を別のクラスにしてみます。

「c_得意先」クラスモジュール

Option Explicit

Public Sub AddNew()
    DoCmd.GoToRecord , , acNewRec
End Sub

Public Sub Update()
    DoCmd.RunCommand acCmdSaveRecord
End Sub

Public Sub Delete()
    DoCmd.SetWarnings False
    DoCmd.RunCommand acCmdDeleteRecord
    DoCmd.SetWarnings True
End Sub

このようにしてフォームがこのメソッドを呼ぶようにします。

「Form_f_得意先」

Option Explicit

Private Model As c_得意先

Private Sub Form_Load()
    Set Model = New c_得意先
End Sub

Private Sub btn更新_Click()
    Model.Update
    CommandMessage “更新”
End Sub

Private Sub btn削除_Click()
    If (MsgBox(“表示中の得意先を削除します。よろしいですか?”, _
                vbInformation + vbYesNo + vbDefaultButton2, “削除確認”) = vbYes) Then
        Model.Delete
        CommandMessage “削除”
    End If
End Sub

Private Sub btn新規_Click()
    Model.AddNew
    CommandMessage “追加”
End Sub

Private Sub CommandMessage(Command As String)
    MsgBox “「” & Command & “」をしました。”
End Sub

「書く場所が違うだけじゃないか」と思った方、そのとおりです。
そもそも機能が同じでコード量が減るはずはないです。
「一箇所に全部書かずに、分散して書く」ことで「コードの見通しのよさ」や「再利用性を高める」というのが目的なのかなと最近思うようになりました。

同じようなコードが続くので次に記事にします。続きの投稿はこちら

クラス内の関数アドレスを取得

標準モジュールの関数に対してはAddressOf演算子で関数のアドレスが取得できます。
しかしクラスの関数のアドレス(ポインタ?)は通常取得できません。

探してみるとありました。
参考URL:http://www.codeguru.com/vb/gen/vb_misc/plugins/article.php/c13883
英語はきつい…インスタンスを生成したあとで「Vartual Function Table(VFT)」という
関数アドレスのテーブル?みたいなものが作られるのでAPIのCopyMemoryで取得して
いろいろしようというものです。

関数(メソッド)を入れ替えます。以下がそのサンプルです。

「Class1」クラス

Public Sub aaa()
    Debug.Print “aaa”
    Swap
End Sub

「Class2」クラス

Public Sub bbb()
    Debug.Print “bbb”
    Swap
End Sub

標準モジュールとテストプロシージャ

Private Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” ( _
    Destination As Any, Source As Any, ByVal Length As Long)

Private Const OffsetToVFT = &H1C
Private pVFT1 As Long
Private pVFT2 As Long

Public Sub Swap()
    Dim fnAddress As Long
    ‘fnAddressに関数Class1の「aaa」のアドレス取得(要するに「Addressof aaa」)
    CopyMemory fnAddress, ByVal pVFT1 + OffsetToVFT, 4
    ‘Class1の「aaa」にClass2の「bbb」をセット(言い方が適当でないかも)
    CopyMemory ByVal pVFT1 + OffsetToVFT, ByVal pVFT2 + OffsetToVFT, 4
    ‘Class2の「bbb」にClass1の「aaa」をセット
    CopyMemory ByVal pVFT2 + OffsetToVFT, fnAddress, 4
End Sub

Public Sub Test()
    Dim c1 As Class1
    Set c1 = New Class1
    Dim c2 As Class2
    Set c2 = New Class2
 
    CopyMemory pVFT1, ByVal ObjPtr(c1), 4
    CopyMemory pVFT2, ByVal ObjPtr(c2), 4
 
    Dim i As Long
 
    For i = 0 To 9
        c1.aaa
    Next
End Sub

「Test」プロシージャの結果は以下のようになります。
イミディエイトウィンドウ

aaa
bbb
aaa
bbb
aaa
bbb
aaa
bbb
aaa
bbb

「c1.aaa」しか呼んでないのに、「c2.bbb」と交互に実行されます。
「Swap」関数で「c1」の最初の関数と「c2」の最初の関数のアドレスを入れ替えているためです。
ちなみに次の関数(というかな?)にアクセスするには
    CopyMemory fnAddress, ByVal pVFT1 + OffsetToVFT + 4, 4

のようにOffsetToVFTに「4」を追加した値でCopyMemoryします。3番目には「8」という具合になるそうです。

標準モジュールの関数で上書きすることは出来ましたが逆はできませんでした。

危険な感じがしますが…オーバーライドとして使えるかも?

VBAコードを見やすくするルール作り

自戒として書いておきます。
・「If Then xxx」の書き方はしない。必ずインデントし「End If」を書く。
・「Dim x As Integer, y As Integer」のように「,」は使わない。
・「On Error」によるエラーハンドリングをほぼ必ず行う。
・「既定のメンバ」による省略を行わない。
・変数名は代入するもののデータ名を使う。(デタラメな名前を付けない)
・Variant型は省略しない。

とりあえずこれくらい。

VBAの擬似継承

Implementsは「親クラスの実装をすべて書く」というのが最大のネックです。
プロパティ「Value1」「Value2」の値を足したり引いたりするだけでも
Implementsした場合は「親クラス_Value1」と「親クラス_Value2」のプロパティの定義が必須です。

違いだけ記述しつつ、多態性を利用するにはどうすればいいか考えました。

最終的にどうなるかというと
・親クラスと対になる操作クラス(自分で命名)を定義
・操作クラスは親クラスのインスタンスを持つ(変数名「This」とする)
・子クラスは親クラスをImplementsして、操作クラスのインスタンスを持つ(変数名「Base」とする)
・操作クラスの親クラスのインスタンス(This)に子クラスを代入する(ImplementsしているのでOK)
・親クラスには多態性を利用したいものだけを定義
・操作クラスでは多態性を利用しないプロパティ等を定義
・操作クラス内のメソッド内では親クラス(This)のメソッドを呼ぶ(→子クラス定義のメソッド)

操作クラスの「This」と子クラスの「Base」を隠蔽するために

操作クラスは「IOperator」クラス
子クラスは「IDerivator」クラス

をそれぞれImplemensするものとしました。

「IOperator」クラス

Option Explicit

Public Property Get Base() As Object
End Property

「 IDerivator」クラス

Option Explicit

Public Property Set This(SubClass As Object)
End Property

「c_Interface」クラス(親クラス)

Option Explicit

‘インターフェイスクラス(多態性の必要なプロシージャのみ)
Public Function Method0()
End Function

「c_Interface_op」クラス(操作クラス)

Option Explicit

Implements IOperator

Public Value1 As Integer
Public Value2 As Integer

Private This As c_Interface

Private Sub Class_Terminate()
    Set This = Nothing
End Sub

‘親クラスのメソッドと呼ぶ

Public Function Method0()
    This.Method0
End Function

Private Property Set IOperator_This(RHS As Object)
    Set This = RHS
End Property

「c_Minus」クラス(子クラス2)

Option Explicit

Implements c_Interface
Implements IDerivator

Private Base As c_Interface_op

Private Function c_Interface_Method0() As Variant
    MsgBox Base.Value1 – Base.Value2
End Function

Private Sub Class_Initialize()
    Set Base = New c_Interface_op
End Sub

Private Sub Class_Terminate()
    Set Base = Nothing
End Sub

Private Property Get IDerivator_Base() As Object
    Set IDerivator_Base = Base
End Property

「c_Plus」クラス(子クラス2)

Option Explicit

Implements c_Interface
Implements IDerivator

Private Base As c_Interface_op

Private Function c_Interface_Method0() As Variant
    MsgBox Base.Value1 + Base.Value2
End Function

Private Sub Class_Initialize()
    Set Base = New c_Interface_op
End Sub

Private Sub Class_Terminate()
    Set Base = Nothing
End Sub

Private Property Get IDerivator_Base() As Object
    Set IDerivator_Base = Base
End Property

標準モジュールとテストプロシージャ

Option Explicit

Public Function Instancing(SubClass As IDerivator) As IOperator
    Set Instancing = SubClass.Base
    Set Instancing.This = SubClass
End Function

Public Function test()
    Dim op As c_Interface_op
 
    ‘切り替えOK
    Set op = Instancing(New c_Minus)
    ‘Set op = Instancing(New c_Plus)
 
    op.Value1 = 20
    op.Value2 = 10
    op.Method0
 
End Function

たった2つのクラスのために4つのクラスを追加していますので結構アレかもしれませんが
同じプロパティを何度も書く必要はなくなりました。

共通のプロパティに対しては子クラスから「Base.」で参照できます。

しかし子クラス独自のメソッドを呼ぶ場合にはやはりキャストが必要になります。