2013年1月27日日曜日

CHtmlViewのスクロール位置を取得する。 (VC++, MFC)

MFCのCHtmlViewクラスはブラウザを表現する為に便利なViewクラスですが、スクロールバーの制御に関してはCOMを扱わないと上手く動作しません。CHtmlViewクラスは下記のようにCScrollViewから派生しています。
CView <-- CScrollView <-- CFormView <-- CHtmlView
CScrollViewのスクロールメソッド を使用すれば制御できそうですが、実際には出来できません。CHtmlViewはWebBrowserコントロールのCOMコンポーネントを使用し表現しており、スクロールバーもWebBrowserコントロールの一部になっている為です。

スクロール位置を取得する為には、MSHTMLコンポーネントをインポートしDOMから制御するしかありません。下記にスクロール位置取得のコードを記載します。
#import <mshtml.tlb> no_auto_exclude auto_rename

static long getScrollTop(CHtmlView* pView){
    long scrollTop = 0;
    
    try {
        MSHTML::IHTMLDocument3Ptr document = pView->GetHtmlDocument();
        MSHTML::IHTMLElement2Ptr  root     = document->documentElement;
        scrollTop = root->scrollTop;

        if (0 == scrollTop){
            MSHTML::IHTMLElement2Ptr body = 
                root->getElementsByTagName(_T("body"))->item(0);
            scrollTop = body->scrollTop;
        }
    }
    catch (_com_error& e){
        // TODO: Error handling...
    }
    return scrollTop;
}
  1. MSHTMLのインポート
    mshtml.tlbを#importする事で、MSHTML名前空間のスマートポインタが使えるようになります。no_auto_excludeは同名の識別子定義があった場合に自動除外(exclude)されるのを拒否します。auto_renameは同名のマクロ定義(inline定義も含む)があった場合に自動的にアンダーバーを二つ付けて定義します。
    "TranslateAccelerator" -> "__TranslateAccelerator"
  2. Documentポインタとルート要素の取得
    ドキュメントポインタに使用するインターフェイスはdocumentElementメンバを持ったIHTMLDocument3を使用しルート要素を取得します。ルート要素ポインタはscrollTopメンバを持ったIHTMLElement2インターフェイスを使用します。
  3. スクロール位置の取得
    <!DOCTYPE html>などDOCTYPE宣言が行われている場合、ルート要素からスクロール位置の取得ができます。しかし、DOCTYPE宣言が行われていない場合、ブラウザは後方互換モードで動作します。その場合、ルート要素からのスクロール位置取得は常にゼロを返します。後方互換モードの場合にスクロール位置を取得するにはbody要素のscrollTopメンバから取得できます。



2013年1月20日日曜日

外部コマンドの実行とリダイレクト(Windows API)

CreateProcess関数で外部コマンドを実行できる。
CUIアプリならsystem関数を使えばよいが、GUIの場合コンソール画面が表示されてしまう。その為、CreateProcess関数で新たに作成したプロセスでコマンドを実行してもらい、終了を待機するという処理を行う。
しかし、実行したコマンドの出力結果をファイルへ書き込みたい場合、CreateProcess関数では"cmd > log.txt" のようなリダイレクトが使えない。">"がコマンドライン引数として扱われてしまうからだ。CreateProcess関数でリダイレクトを実現する為には、CreateFile関数で取得したファイルハンドルを出力先に設定する必要がある。下記にCreateFile関数とCreateProcess関数で必要な手続きを記載する。

CreateFile関数

  1. セキュリティ属性でハンドルを起動したプロセスで使用する事を許可する。
  2. 新たに起動させるプロセスに書き込みさせる為、共有モードで開く。
  3. 起動したプロセスが終了したら、ファイルハンドルを閉じる。

CreateProcess関数

  1. セキュリティ属性で作成するプロセスとスレッドでのハンドル使用を許可する。
  2. 標準出力ハンドルにファイルハンドルを渡す。
  3. CreateProcess関数を実行し、WaitForSingleObject関数で終了を待機する。
  4. コマンドの終了コードを取得する。
  5. 使い終わったハンドルを閉じる。
DWORD execute(LPCTSTR lpCommand, LPCTSTR lpRedirectFile) {
    DWORD dwExidCode = -1;

    TCHAR* pCmd;
    size_t nSize = _tcsclen(lpCommand);
    pCmd  = new TCHAR[nSize +1];
    _tcscpy_s(pCmd, nSize+1, lpCommand);

    SECURITY_ATTRIBUTES sa;
    sa.nLength              = sizeof(sa);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle       = TRUE;        

    HANDLE hFile = CreateFile(
        lpRedirectFile,
        GENERIC_WRITE,
        FILE_SHARE_WRITE,
        &sa,                    
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if (hFile != INVALID_HANDLE_VALUE){

        STARTUPINFO si;
        ZeroMemory(&si, sizeof(si));
        si.cb         = sizeof(si);    
        si.dwFlags    = STARTF_USESTDHANDLES;
        si.hStdOutput = hFile;

        PROCESS_INFORMATION pi;
        ZeroMemory(&pi, sizeof(pi));

        if(CreateProcess(NULL, pCmd, &sa, &sa, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)){
            WaitForSingleObject(pi.hProcess, INFINITE );
            GetExitCodeProcess(pi.hProcess, &dwExidCode); 
            CloseHandle(pi.hProcess);
            CloseHandle(pi.hThread);
        }
        CloseHandle(hFile);
    }

    delete[] pCmd;
    return dwExidCode;
}
※CreateProcess関数に渡すコマンド文字列はconst付きの文字列を取らないので、ヒープ上にコピーしたうえで渡す。

2013年1月15日火曜日

MFC ファイル保存ダイアログのファイル形式を増やす。

MFC CDocumentクラスの保存ダイアログのカスタマイズ。
保存形式(拡張子)の候補を増やす方法。


CDocumentから派生させたクラスを作成しておき、
CDocument::DoSaveメソッドをオーバーライドする。
virtual BOOL DoSave(LPCTSTR lpszPathName, BOOL bReplace = TRUE);

CDocumentの既存のコードを丸ごとオーバーライドさせたメソッドにコピーしておく。
コードはdoccore.cpp(C:\Program Files\Microsoft Visual Studio 10.0\VC\atlmfc\src\mfc\doccore.cpp ※VisualStudio2010の場合)から取得する。
afxdatarecovery.hヘッダや、DELETE_EXCEPTIONマクロはdoccore.cppに記述されていたものでビルドを通す為にこれも一緒にコピーしておく。(DELETE_EXCEPTIONマクロはdoccore.cppがインクルードしていたstdafx.hから取得)
下記、コピーするソースコード。
#include "io.h" // for _access
#include "afxdatarecovery.h"

#define DELETE_EXCEPTION(e) do { if(e) { e->Delete(); } } while (0)

BOOL CDocument::DoSave(LPCTSTR lpszPathName, BOOL bReplace)
    // Save the document data to a file
    // lpszPathName = path name where to save document file
    // if lpszPathName is NULL then the user will be prompted (SaveAs)
    // note: lpszPathName can be different than 'm_strPathName'
    // if 'bReplace' is TRUE will change file name if successful (SaveAs)
    // if 'bReplace' is FALSE will not change path name (SaveCopyAs)
{
    CString newName = lpszPathName;

    if (newName.IsEmpty())
    {
        CDocTemplate* pTemplate = GetDocTemplate();
        ASSERT(pTemplate != NULL);

        newName = m_strPathName;
        if (bReplace && newName.IsEmpty())
        {
            newName = m_strTitle;
            // check for dubious filename
            int iBad = newName.FindOneOf(_T(":/\\"));
            if (iBad != -1)
                newName.ReleaseBuffer(iBad);

            if (AfxGetApp() && AfxGetApp()->GetDataRecoveryHandler())
            {
                // remove "[recovered]" from the title if it exists
                CString strNormalTitle = AfxGetApp()->GetDataRecoveryHandler()->GetNormalDocumentTitle(this);
                if (!strNormalTitle.IsEmpty())
                    newName = strNormalTitle;
            }

            // append the default suffix if there is one
            CString strExt;
            if (pTemplate->GetDocString(strExt, CDocTemplate::filterExt) && !strExt.IsEmpty())
            {
                ASSERT(strExt[0] == '.');
                int iStart = 0;
                newName += strExt.Tokenize(_T(";"), iStart);
            }
        }

        if (!AfxGetApp()->DoPromptFileName(newName,
          bReplace ? AFX_IDS_SAVEFILE : AFX_IDS_SAVEFILECOPY,
          OFN_HIDEREADONLY | OFN_PATHMUSTEXIST, FALSE, pTemplate))
            return FALSE;       // don't even attempt to save
    }

    CWaitCursor wait;

    if (!OnSaveDocument(newName))
    {
        if (lpszPathName == NULL)
        {
            // be sure to delete the file
            TRY
            {
                CFile::Remove(newName);
            }
            CATCH_ALL(e)
            {
                TRACE(traceAppMsg, 0, "Warning: failed to delete file after failed SaveAs.\n");
                DELETE_EXCEPTION(e);
            }
            END_CATCH_ALL
        }
        return FALSE;
    }

    // reset the title and change the document name
    if (bReplace)
    {
        SetPathName(newName);
        OnDocumentEvent(onAfterSaveDocument);
    }

    return TRUE;        // success
}


DoSaveメソッドは「上書き保存」や、「名前を付けて保存」とした時に呼び出されるメソッドで、lpszPathNameにファイル名が設定されていない場合にファイル選択ダイアログを表示してファイル名を取得している。
DoPromptFileNameメソッドを呼び出しているあたりがファイル選択ダイアログを表示している部分だ。
DoPromptFileNameメソッドは、CWinAppクラスがインスタンスを保持しているCDocManagerクラスのメソッドで、実装はdocmrg.cpp(C:\Program Files\Microsoft Visual Studio 10.0\VC\atlmfc\src\mfc\docmgr.cpp ※VisualStudio2010の場合)に書かれている。内容はCFileDialogを使ってファイル選択ダイアログを表示しているが、その際にCDocTemplateクラスからアプリケーション固有の拡張子を取得し追加しているようだ。MFCウィザードで作成した際に聞かれた拡張子の部分だ。
BOOL CDocManager::DoPromptFileName(CString& fileName, UINT nIDSTitle, DWORD lFlags, BOOL bOpenFileDialog, CDocTemplate* pTemplate)
{
    CFileDialog dlgFile(bOpenFileDialog, NULL, NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, NULL, NULL, 0);

    CString title;
    ENSURE(title.LoadString(nIDSTitle));

    dlgFile.m_ofn.Flags |= lFlags;

    CString strFilter;
    CString strDefault;
    if (pTemplate != NULL)
    {
        ASSERT_VALID(pTemplate);
        _AfxAppendFilterSuffix(strFilter, dlgFile.m_ofn, pTemplate, &strDefault);
    }
    else
    {
        // do for all doc template
        POSITION pos = m_templateList.GetHeadPosition();
        BOOL bFirst = TRUE;
        while (pos != NULL)
        {
            pTemplate = (CDocTemplate*)m_templateList.GetNext(pos);
            _AfxAppendFilterSuffix(strFilter, dlgFile.m_ofn, pTemplate,
                bFirst ? &strDefault : NULL);
            bFirst = FALSE;
        }
    }

    // append the "*.*" all files filter
    CString allFilter;
    VERIFY(allFilter.LoadString(AFX_IDS_ALLFILTER));
    strFilter += allFilter;
    strFilter += (TCHAR)'\0';   // next string please
    strFilter += _T("*.*");
    strFilter += (TCHAR)'\0';   // last string
    dlgFile.m_ofn.nMaxCustFilter++;

    dlgFile.m_ofn.lpstrFilter = strFilter;
    dlgFile.m_ofn.lpstrTitle = title;
    dlgFile.m_ofn.lpstrFile = fileName.GetBuffer(_MAX_PATH);

    INT_PTR nResult = dlgFile.DoModal();
    fileName.ReleaseBuffer();
    return nResult == IDOK;
}

今回はDoPromptFileNameメソッドの変わりに、自分でCFileDialogクラスを呼び出す事で、選択出来るファイル形式(拡張子)を増やす事とした。
下記がDoPromptFileNameメソッドを呼び出す代わりにオーバーライドしたDoSaveメソッド内に書いたコード。
CFileDialog dlgFile(FALSE, _T("md"), NULL, 
    OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_LONGNAMES,
    _T("Markdown      (*.md)|*.md|")
    _T("Text          (*.txt)|*.txt|")
    _T("HTML          (*.html;*.htm)|*.html;*htm|")
    _T("全てのファイル(*.*)|*.*||"),
    NULL, 0);

CString title;
ENSURE(title.LoadString(bReplace ? AFX_IDS_SAVEFILE : AFX_IDS_SAVEFILECOPY));
dlgFile.m_ofn.lpstrTitle = title;
dlgFile.m_ofn.lpstrFile = newName.GetBuffer(MAX_PATH);
INT_PTR nResult = dlgFile.DoModal();
newName.ReleaseBuffer();
if (IDOK != nResult) return FALSE;
CFileDialogクラスのコンストラクタ関数で、第二引数にデフォルトの拡張子を設定しておくと、
ファイル名に自動で拡張子を付けてくれるようになる。



2013年1月13日日曜日

CHtmlViewでブラウジング時の音を消す。

CHtmlViewでNavigate2メソッドを使うたびにIEがカチカチと音を鳴らしてしまう。
IWebBrowserAppインターフェイスを使うものではこれが鳴ってしまうそうだ。

コントロールパネルから、IEの環境設定で消す事ができるみたいだが、
アプリケーションのみに適用したい場合は、CoInternetSetFeatureEnabled関数を使えばいい。
第二引数でSET_FEATURE_ON_PROCESSを指定しているので、
この関数を呼び出したプロセスにのみ反映させる事ができている。
#include <UrlMon.h>

CoInternetSetFeatureEnabled(
    FEATURE_DISABLE_NAVIGATION_SOUNDS,
    SET_FEATURE_ON_PROCESS,
    TRUE);

第一引数につかえる定数
enum _tagINTERNETFEATURELIST{
    FEATURE_OBJECT_CACHING = 0,
    FEATURE_ZONE_ELEVATION,
    FEATURE_MIME_HANDLING,
    FEATURE_MIME_SNIFFING,
    FEATURE_WINDOW_RESTRICTIONS,
    FEATURE_WEBOC_POPUPMANAGEMENT,
    FEATURE_BEHAVIORS,
    FEATURE_DISABLE_MK_PROTOCOL,
    FEATURE_LOCALMACHINE_LOCKDOWN,
    FEATURE_SECURITYBAND,
    FEATURE_RESTRICT_ACTIVEXINSTALL,
    FEATURE_VALIDATE_NAVIGATE_URL,
    FEATURE_RESTRICT_FILEDOWNLOAD,
    FEATURE_ADDON_MANAGEMENT,
    FEATURE_PROTOCOL_LOCKDOWN,
    FEATURE_HTTP_USERNAME_PASSWORD_DISABLE,
    FEATURE_SAFE_BINDTOOBJECT,
    FEATURE_UNC_SAVEDFILECHECK,
    FEATURE_GET_URL_DOM_FILEPATH_UNENCODED,
    FEATURE_TABBED_BROWSING,
    FEATURE_SSLUX,
    FEATURE_DISABLE_NAVIGATION_SOUNDS,
    FEATURE_DISABLE_LEGACY_COMPRESSION,
    FEATURE_FORCE_ADDR_AND_STATUS,
    FEATURE_XMLHTTP,
    FEATURE_DISABLE_TELNET_PROTOCOL,
    FEATURE_FEEDS,
    FEATURE_BLOCK_INPUT_PROMPTS,
    FEATURE_ENTRY_COUNT 
} INTERNETFEATURELIST;



2013年1月12日土曜日

SDIでマルチビューの実装(VC++ MFC)

Windows Visual Studio 2010 MFC
SDI(Single Document Interface)でマルチビューを使う。

MFCアプリケーションのウィザードから作成。

「SDI」にチェック
「ドキュメント/ビュー アーキテクチャのサポート」にチェック
「分割ウィンドウ」をチェック


動的生成の機能を備えたViewクラスを用意しておく。
(DECLARE_DYNCREATEマクロとIMPLEMENT_DYNCREATEマクロで定義したクラス)

「分割ウィンドウ」をチェックしているので、CMainFrameにはCSplitterWndのメンバ変数m_wndSplitterが定義されている。ウィザードで作成した状態では同じViewを分割して表示するだけなので、OnCreateClientメソッドを書き換える。
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT  lpcs,
                                CCreateContext* pContext)
{
    // 分割ウィンドウの作成
    m_wndSplitter.CreateStatic(this, 1, 2);

    // Create FirstView 
    CRuntimeClass*  firstViewClass = RUNTIME_CLASS( CFirstView);
    pContext->m_pNewViewClass = firstViewClass;
    m_wndSplitter.CreateView(0,0, 
        pContext->m_pNewViewClass, CSize(0,0), pContext);

    // Create SecondView
    CRuntimeClass* secondViewClass = RUNTIME_CLASS(CSecondView);
    pContext->m_pNewViewClass = secondViewClass;
    m_wndSplitter.CreateView(0,1, 
        pContext->m_pNewViewClass, CSize(0,0), pContext);

    return TRUE;
}

CWinAppから派生したCMyAppのInitInstanceメソッドで記述されているCSingleDocTemplateのViewの引数はNULLにしておく。
BOOL CMyApp::InitInstance()
{
    CWinAppEx::InitInstance();

    // SDI
    CSingleDocTemplate* pDocTemplate = new CSingleDocTemplate(
        IDR_MAINFRAME,
        RUNTIME_CLASS(CMyDoc),
        RUNTIME_CLASS(CMainFrame),
        NULL);                    // ViewにNULLを渡しておく
    if (!pDocTemplate)
        return FALSE;
    AddDocTemplate(pDocTemplate);

    m_pMainWnd->ShowWindow(SW_SHOW);
    m_pMainWnd->UpdateWindow();
    return TRUE;
}

2013年1月9日水曜日

peg-markdownのコンパイル (MinGW)

Windows環境でのMarkdownエディタを作りたかったので、peg-markdownというものを使ってみようと思いました。peg-markdownはC言語で記述されている為、動作も早く軽快なエディタが作れるかもしれないと思ったからです。githubにあるソースから、MinGWを使用してpeg-markdownだけでもコンパイルしてみたので忘備録。

環境: MinGW on Windows7


<用意するもの>


MinGWの環境
MinGWのサイトリンクから、SourceForgeにある最新のインストーラをダウンロード
http://sourceforge.net/projects/mingw/files/
インストール画面のツール群全てにチェックを入れてインストール実行。

MinGW Shell (msys)を起動、wgetコマンドのインストール
$ mingw-get install msys-wget

GLib
GTK+ のサイトから、Windows 32bit用のバイナリファイルをダウンロード
http://www.gtk.org/download/win32.php
pkg-config
GLib
zlib
gettext
 等々が必要になるが、”all-in-one-bundle”というのがあるので、
そこから全てダウンロード。
MinGW環境にそれぞれディレクトリを作成しておき。
/usr/local/bin
/usr/local/include
/usr/local/lib
そこにそれぞれのファイルを配置する。


peg-markdown
Githubからダウンロード(https://github.com/jgm/peg-markdown)
MinGWの環境内にダウンロード。


<コンパイル>

peg-markdownのReadMeを参考に、
付属するpegというライブラリをmakeする。

$ cd peg-0.1.4
$ make PKG_CONFIG=c:/path/to/glib/bin/pkg-config.exe
※/usr/local/bin内にpkg-config等はインストールしている為、変数は指定しなくてもよい。

peg-markdownディレクトリに戻って、makeする。
$ cd ../
$ ./configure
$ make

markdown.exeが生成されるので、Markdownファイルを渡すとhtmlファイルを生成できる。
$ markdown input.md -o output.html

MinGW環境でコンパイルしたmarkdown.exeは、下記dllファイルを一緒に配布すれば、
WindowsXP, 7環境で問題なく動作するようでした。
intl.dll
libglib-2.0-0.dll
msvcrt.dll

日本語を含むMarkdownファイルや、
文字コードがshift-jis, utf-8でもちゃんと変換できていました。







2013年1月4日金曜日

PerlのDBIモジュールを使ってSQLite3データベースを利用する。

最近、Perlの勉強を始めました。
Perlって取っ付きにくい感じがしてたんですが、Unix, Linux環境ではデフォルトでインストールされてるしモジュールが充実してるのでやりたい事を手っ取り早く実行できるじゃん!
って感動してます。
モジュールのインストールはppmやcpan、更にはapt-getで管理できちゃう。
やっぱりapt-getが一番楽な感じです。
数行のコーディングでもかなりの機能が実装できそうな言語ですね。

下記、Ubuntu12.04でPerlのDBIからSQLite3データベースを使う時の忘備録を記載します。
  1. perldocコマンドのインストール

    Perlのドキュメントを参照できるコマンドをインストールする。
    なくても良いけど、モジュールのPATHとかも調べられるようなのであると便利かと思う。
    $ sudo apt-get install perl-doc

    # Perlモジュールのドキュメントを読む。
    perldoc <モジュール名>


    # Perlモジュールのインストール先を調べる。
    perldoc -ml <モジュール名>


  2. DBD::SQLiteをインストール

    SQLiteを扱う為のDBD::SQLiteモジュールをインストールする。
    $ sudo apt-get install libdbd-sqlite3-perl

    apt-getでインストールする場合は、基本的に先頭にlibを付けて、小文字で表記。 ”::”は”-”に変わり、最後にPerlモジュールを表す -perl が付くようだ。
    apt-cache search dbd-sqlite などと検索すると良い。

- SQLite3 を利用するPerlのサンプルスクリプト -
use DBI
my $dbfile = "sample.sqlite3";
my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile");

# TODO:

$dbh->disconnect();
undef($dbh);