プログラム悪戦苦闘日記

はてなダイアリーからの移行(遺物)

GUIはどーなってるの

 Excel VBAの話の続きです。Excelの操作性とかそういう話ではなく、GUIプログラムはどういう構成はどうなっているか、という話です。

 GUIプログラムにおいて、最初に気になる点は次の4つではないかと思っている。

  1. コンポーネントのディスパッチャー(イベント送出者)は誰か?
  2. イベントハンドラは、どのように定義/実装されるか?
  3. イベントディスパッチャーとイベントハンドラはどのように関係付けるか?
  4. スレッドモデルは何か?

 1番はあまり意識する必要がないので、意味が分からないかもしれないが、早く言うとイベントの発行したオブジェクトは何か、ということである。たとえば、Java GUIプログラムの場合、ボタンを押したならButtonオブジェクト自身ということになる。通常は、ボタンを押したりテキストボックスに値を入れたりしたときは、そのコンポーネント自身がイベント発行者になる。
 2番は、JavaでいうところのListenerである。Listenerインターフェイスをimplementしたクラスがイベントハンドラになる。(正確にはイベントハンドラの候補になる、といったほうが良いのかも)
 3番目は、イベントハンドラをディスパッチャーに登録する方法である。Javaなら、addXxxListenerでイベントハンドラとディスパッチャーを関連付ける。
 4番目は、イベント処理のスレッドモデルである。Java Swingならシングルスレッドである。最近のGUIライブラリなら、大抵はシングルスレッドだろう。

 では、Excel VBAはどうなっているだろうか。
1→イベントを発行したコンポーネント自身
2→イベントを発行したコンポーネントが張り付いている、一番親のコンポーネント(通常はシートオブジェクトになる)
3→イベントを発行したオブジェクト(の親オブジェクト)がハンドラになっているから、最初から関連付いている
4→たぶんシングルスレッドだと思われ
 なんとまぁ、MFCと同じでつか。特にイベントハンドラがディスパッチャーと同じだから、処理が一箇所にかたまってしまう(平たく言うと、処理がみーんなシートオブジェクトのモジュールに書かれてしまう)。だから、下手なExcel VBAプログラムは、処理がみーんなシートやブックのモジュールに固まっていることで見分けられたりする。…いちよう断っておきますが、これはExcel VBAをアプリケーションで開発する場合での話であって、ちょっとしたツールやドキュメントの整理のために作るExcel VBAは、この話の範囲ではないです。この手のツールは、早く作ることが重要であって、プログラムの見通しの良さは2の次になりますから。

 話し戻して、とにかく処理が一箇所に固まってしまうので、何も考えないで作るとプログラムの見通しが悪くなる。ボタンを押してセルになんか色を付ける処理と、ボタンを押してグラフを描く処理が同じモジュールに書けば、そりゃみにくくなるでしょ。
 で、この対策だが、やはり処理を委譲するしかない。MFCならCWnd::DefWindowProc()とかそんなメソッドをオーバーライドして、メッセージマップに逝く前に、処理をかっさらったりすることも出来るが、VBAはムリ。だから、とりあえずイベントハンドラはシートモジュールに書いておいて、すぐさま別の(標準)モジュールの関数に処理を渡してしまう(委譲)という書き方になるだろう。が、ここで問題が…
 前回も書いたが、Excel VBAは構造化プログラムに近く、モジュール志向であるということだ。つまり、データの受け渡しが苦手/面倒臭いということだ。必要なデータはみんな関数の引数に渡してやる必要がある。だから関数の引数が異様に増える傾向になる。昔なつかしきC言語のようなプログラムだ。だからC言語と同じ過ちがVBAでも繰り返されることになる。つまりグローバル変数地獄だ。だから下手なVBAグローバル変数が多くなる。

 話が散漫になってしまったが、何が言いたいかというと、Excel VBAGUIプログラムに向かないということだ。経験者にしか分からない例えだが、C言語でX Windowシステムのプログラムを書くのに似ている、ということだ。ちなみに私はX Windowプログラムなんて書いたことないですよ^ ^;

Excel VBAはモジュール志向!

 時間無いので要点だけ。

  • VBAはモジュール(≒ファイル)か、スコープの単位になる
  • 一言で言うと、『ポインタのないC言語』
  • VBAで言うところの「オブジェクト」とは、『メソッド付き構造体』(いちようカプセル化はあるが…)
  • クラスモジュールは、オブジェクト指向のクラス(オブジェクト)というよりは、メソッド付きデフォルトコンストラクタが書ける『構造体』

Excel VBA 事始め

 他言語で開発経験を積んでいると、Excel VBAが非常にとっつきにくい。実はExcel VBAを『開発言語』と捉えると、とても奇特な環境でなのである。というのは、Excel ワークブックというのは、

  • 開発用のエディタ内蔵
  • デバッグ環境
  • 配布用実行ファイル

の3つを同時に満たしているからだ。通常の開発言語は、『統合IDE』と称して上2つが、一緒になっていることがあるが、さすがに配布ファイルも一緒になることはない。しかし、Excelはそうなるのだ。これは、自分の書いたプログラムも一緒に配布されてしまう、ということを意味する。いちようコードは暗号化できるので、利用者側に見せないようにすることはできるが、なんとも気持ち悪い感じがしてしまう。

 また、Excel VBAの特異な点については、ユーザー環境で確実に実行される保証が無い、ということだろうか。これは、1度は見たことがあると思うが、VBA
付きのファイルを開くと、マクロがありますが実行しますか? というダイアログが表示されてしまうことだ。これでNoを選択されてしまうと、どうにもならない。よく、マクロを強制的に実行させる方法はありませんか、と聞かれるが、こればっかりはどうにもならない。

 Excel VBA秀丸マクロのような感覚で利用する分には便利なんだろうけど、ひとたび『アプリケーション』規模で『開発』してしまうと、これほど厄介なものは無い。あぁ、なんでこんなんでアプリケーションを開発しようとか考えるのかな。そりゃ、実行環境を考えなくて良い(どこでもExcelはインストールされているだろうから)からなんだろうけど…

攻略サイトをねこぞぎ奪う

こんなん作った。かなり手抜きだけど

require 'net/http'
require 'socket'
require 'URL.rb'

exit if ARGV.length != 1

def mkdir(dir)
    Dir.mkdir(dir) unless File.exist?(dir)
end

def parse_html(url)
    puts url.to_s
    
    tokens = []

    begin
        Net::HTTP.start(url.domain, 80) { |http|
            str = ""
    
            response = http.get(url.site)
            response.body.split(/([<>])/m).each { |t|
                case t
                when /</
                    tokens.push(str) if str != ""
                    str = t
            
                when />/
                    tokens.push(str + t)
                    str = ""

                else
                    str += t
                end
            }
    
            tokens.push(str) if str != ""
    
            tokens
        }
    rescue
        puts "[" + url.to_s + "]取得失敗..." 
    end
end

def put_html(url, tokens)
    anchor = []
    
    open(url.filename, "w") { |f|
        tokens.each { |token|
            case token
            when /<link/i
                elements = parse_tag(token)
                
                if elements["rel"] == "stylesheet"
                    resource_url = url.make_url(elements["href"])
                    load_resource(resource_url, "css")
                    
                    elements["href"] = "./css/" + resource_url.filename
                    f.print "<link" + make_tag(elements) + ">"
                else
                    f.print token
                end
            
            when /<img/i
                elements = parse_tag(token)
                
                resource_url = url.make_url(elements["src"])
                
                # 同一domainのときだけ、リソースを取得する
                if resource_url.domain == url.domain
                    load_resource(resource_url, "img")
                
                    elements["src"] = "./img/" + resource_url.filename
                    f.print "<img" + make_tag(elements) + ">"
                end
                
            when /<a/i
                elements = parse_tag(token)
                
                if url.level < 1 && elements.include?("href")
                    anchor_url = url.make_url(elements["href"])
                    
                    if anchor_url.domain == url.domain
                        anchor.push(anchor_url)
                        elements["href"] = anchor_url.filename
                        f.print "<a" + make_tag(elements) + ">"
                    else
                        f.print token
                    end
                else
                    f.print token
                end
                
            else
                f.print token
            end
        }
    }
    
    anchor
end

def parse_tag(str)
    elements = {}
    
    str.scan(/\w+\s*=['"].+?['"]/).each { |token|
        elements[token.split("=")[0].strip.downcase] = token.split("=")[1].gsub(/['"]/, "").strip
    }

    elements
end

def make_tag(elements)
    str = ""
    elements.each { |key, value|
        str += " #{key}=\"#{value}\""
    }
    str
end

def load_resource(url, type)
    open((type.nil?? "" : "./#{type}/") + url.filename, "wb") { |f|
        Net::HTTP.start(url.domain, 80) { 
            |http| f.print http.get(url.site).body
        }
    }
end





Net::HTTP.version_1_2   # おまじない

urls = [URL.new(ARGV[0])]

mkdir("img")
mkdir("css")

urls.each { |url|
    tokens = parse_html(url)
    urls.push(put_html(url, tokens))
    urls.flatten!; urls.compact!
}

URLクラスはこんなんです

class URL
    def initialize(url, level = 0)
        @level = level
        
        url = url[("http://".length)..-1] if url.length >= "http://".length && url =~ /^http:\/\//
        
        @str = url.split(/\//, -1)
        @str.push("") if @str.length == 1
        
        @str[-1] = @str[-1].split(/\?/)[0] if @str[-1].include?("?")
    end
    
    def domain
        @str[0]
    end
    
    def site
        "/" + @str[1..-1].join("/")
    end
    
    def filename
        if @str[-1] == ""
            "index.html"
        else
            @str[-1]
        end
    end
    
    def level
        @level
    end
    
    def make_url(url)
        case url
        # 完全URL
        when /^http:\/\//
            URL.new(url, self.level + 1)
            
        # 絶対URL
        when /^\// 
            URL.new(self.domain + url, self.level + 1)
        # 相対URL
        else 
            URL.new(@str[0...-1].join("/") + "/" + url, self.level + 1)
        end
    end
    
    def to_s
        @str.join("/")
    end
end

Windowsでバックアップをとりたい

 環境:Windows XP。mswin版のRuby 1.8.2がインストールしてある
 やりたいこと:ローカルドライブ(Dドライブ)にある、あるフォルダ以下をネットワーク上にあるコンピューターのあるフォルダにコピーする。そのとき、そのネットワーク上のコンピュータのフォルダをアーカイブにしてzip圧縮しておく。つまり、

D:\Hoge\         ← バックアップもとのフォルダ
D:\Hoge\aaa\     ← バックアップ対象
D:\Hoge\bbb.xls ← バックアップ対象

\\Foo\Piyo\        ← バックアップ先のフォルダ
\\Foo\Piyo\aaa\    ← 前回バックアップしたフォルダ
\\Foo\Piyo\bbb.xls ← 前回バックアップしたファイル
\\Foo\Piyo\backup_20050830.zip ← 前々回バックアップしたファイル/フォルダ

これを
\\Foo\Piyo\ 以下を圧縮して(zipは除く)backup_20050831.zip としてこのフォルダに保存。
zipにしたファイル/フォルダは削除
D:\Hoge\ 配下を \\Foo\Piyo\ にコピー

である。これをWindowsのコマンド(DOSコマンド、か?)とmswin版Rubyでどうやるか。
 これ、一見簡単に見えるが、ハマり要素がたくさんある。つーか結論を先にいってしまうと『不可能』である。UNIXでは簡単なのだが…。本当に嫌になりましたorz。

2005/7/19の日記

 スパムメールについて書くと、スパマーのえさになるのではないかと思っていたが、やっぱりなっていた。もしそうなっていたときは日記を削除すればいいと思っていたが…。日記は1日単位で消せないのね。消せるのかもしれないが調べるのが面倒くさい。メニューの「日記の削除」は全削除だし。これ使えない。

Excel VBA オブジェクト階層図のウソ

 今までなんとかVBAを使った開発を逃げてきたのだが、今回ばかりはどうしてもExcel VBAでの開発になってしまった。Excel VBAを触ったことがないわけではないのだが、まともにやるのは今回が初。そこで文法とかAPIとかオブジェクトとか適当に勉強していたのだが、よくわからなかったのが、この「オブジェクト階層図」。これは、こんな風になっているのだが、

┌──────┐
|Application |
└──────┘
 ┃┌──────┐
 ┗|Workbook(s) |
  └──────┘
   ┃┌──────┐
   ┗|Worksheet(s)|
    └──────┘
     ┃┌──────┐
     ┗|Range       |
      └──────┘

これは、どう見ても RangeはApplicationのサブクラス、に見えるのだが…。しかし、どう考えてもRangeがApplicationであるはずがない。そこで3分でできる!Excel VBA、という本をよく見てみたら、ApllicatonはWorkbook(s)オブジェクトを持っている、とか書いてあるし。つまりExcel VBAのオブジェクト階層図は、すべてhas-a関係を表している、ということだ。だからApplicationがもっているメソッドやらプロパティやらはRangeに継承される、ということはないのである。
 こんなことは、VBAを使っている人には当然なのだろうが…。クラスの継承と同じ表記にするなと。上にあるクラスを「親クラス」と呼ぶなと。見た目がMFCとそっくりというのが、なんともMSらしい書き方です。