この記事は、初心者C++er Advent Calendar 2016の4日目の記事です。


C++で、数値を文字列に変換する関数といえば、まずはstd::to_string (cpprefjp)です。ところが、この関数は、十進法で結果が出てきます。ほかによく使うものと言えば、十六進法です。というわけで、十六進法で文字列に変換する方法の話です。

標準ライブラリにいい感じのものがないので、Boostに頼ります。使いますのは、hexという単純な名前の関数です。

#include <iostream>
#include <string>
#include <iterator>
#include <cstdint>
#include <boost/algorithm/hex.hpp>
 
int main()
{
  std::uint32_t x = 0xdeadbeaf;
  std::string s;
  boost::algorithm::hex(&x, &x + 1, std::back_inserter(s));
  std::cout << s << std::endl;
}

このプログラムを実行すると、変数sの中身は”DEADBEAF”となり、それが出力されます。

hex関数は、「イテレータ2つまたはRangeで複数要素を指定し、一気に変換する関数」という設計になっています。ところが、このように1要素だけ変換するのに使うこともできるんです。

というわけで、boost::algorithm::hex関数を使い、数値オブジェクト1要素を文字列に変換する、という内容でした。こういう使い方、意外と見かけなかったので、今回紹介することにしました。

リファレンス: The Boost Algorithm Library – hex

この記事のカテゴリ

  • ⇒ 十六進数で文字列に変換する (Boostのhex関数)

wstring_convertやwbuffer_convertを使ってワイド文字列とマルチバイト文字列との変換を実現する方法を考えました。

wstring_convertやwbuffer_convertは、前回(VC++のwstring_convertやwbuffer_convertがちょっと変)書いたように、その名前に反してワイド文字との変換に使えるようになっていません。std::use_facetの戻り値を使えるように作られていないためです。

そこをどうにかできないかとずっと考えていて、やっと思いつきました。use_facetで得たcodecvtに移譲するクラスを作るという手法です。それをwstring_convertやwbuffer_convertに渡せばいいわけです。

#include <array>
#include <iostream>
#include <locale>
#include <codecvt>
#include <string>
 
template<typename CodeCvt>
class codecvt_forwarder : public CodeCvt
{
public:
  //using std::codecvt_base::result; // ← Visual C++ 2015でダメだった。
  using typename CodeCvt::result;
  using typename CodeCvt::state_type;
  using typename CodeCvt::intern_type;
  using typename CodeCvt::extern_type;
 
  explicit codecvt_forwarder(const std::locale& loc)
    : CodeCvt(), m_loc(loc), m_cvt(std::use_facet<CodeCvt>(m_loc))
  {
  }
 
  ~codecvt_forwarder() {}
 
  result do_out(
    state_type& state,
    const intern_type* from,
    const intern_type* from_end,
    const intern_type*& from_next,
    extern_type* to,
    extern_type* to_end,
    extern_type*& to_next) const override
  {
    return m_cvt.out(state, from, from_end, from_next, to, to_end, to_next);
  }
  result do_unshift(
    state_type& state,
    extern_type* to,
    extern_type* to_end,
    extern_type*& to_next) const override
  {
    return m_cvt.unshift(state, to, to_end, to_next);
  }
  result do_in(
    state_type& state,
    const extern_type* from,
    const extern_type* from_end,
    const extern_type*& from_next,
    intern_type* to,
    intern_type* to_end,
    intern_type*& to_next) const override
  {
    return m_cvt.in(state, from, from_end, from_next, to, to_end, to_next);
  }
  int do_encoding() const noexcept override
  {
    return m_cvt.encoding();
  }
  bool do_always_noconv() const noexcept override
  {
    return m_cvt.always_noconv();
  }
  int do_length(
    state_type& state,
    const extern_type* from,
    const extern_type* from_end,
    std::size_t max) const override
  {
    return m_cvt.length(state, from, from_end, max);
  }
  int do_max_length() const noexcept override
  {
    return m_cvt.max_length();
  }
 
private:
  std::locale m_loc;
  const CodeCvt& m_cvt;
};
 
class wstring_convert_by_locale
  : public std::wstring_convert<
    codecvt_forwarder<std::codecvt<wchar_t, char, std::mbstate_t>>, wchar_t>
{
public:
  wstring_convert_by_locale(const std::locale& loc)
    : wstring_convert(
      new codecvt_forwarder<std::codecvt<wchar_t, char, std::mbstate_t>>(loc))
  {
  }
};
 
class wbuffer_convert_by_locale
  : public std::wbuffer_convert<
    codecvt_forwarder<std::codecvt<wchar_t, char, std::mbstate_t>>, wchar_t>
{
public:
  wbuffer_convert_by_locale(std::streambuf* buf, const std::locale& loc)
    : wbuffer_convert(
      buf,
      new codecvt_forwarder<std::codecvt<wchar_t, char, std::mbstate_t>>(loc))
  {
  }
};
 
int main()
{
  std::locale loc("");
  std::locale::global(loc); // setlocaleを呼び出すことを意図している。
 
  wstring_convert_by_locale cv(loc);
 
  wbuffer_convert_by_locale buf(std::cin.rdbuf(), loc);
  std::wistream my_wcin(&buf);
 
  std::wstring input;
  std::getline(my_wcin, input);
 
  std::array<char, 256> input_mbs;
  wcstombs(input_mbs.data(), input.c_str(), input_mbs.size());
 
  std::wstring wcs = cv.from_bytes(input_mbs.data());
  std::string mbs = cv.to_bytes(wcs);
 
  std::cout << mbs << std::endl;
}

クラステンプレートcodecvt_forwarderは、codecvtの全virtualメンバー関数をオーバーライドしています。いずれも、すべてメンバー変数m_cvtのpublicメンバー関数に処理を丸投げしています。m_cvtはuse_facetで得たオブジェクトです。

変換に使うcodecvtについては、std::codecvt_byname<char, wchar_t, std::mbstate_t>を使う方法も考えられます。一応汎用性を重視して、上記プログラムではstd::localeとstd::use_fasetの組み合わせとしました。

FreeBSDでこんな感じで動きました。

$ echo あいうえお | iconv -t EUC-JP | env LANG=ja_JP.eucJP ./a.out | iconv -f EUC-JP
あいうえお
$ echo あいうえお | iconv -t Shift_JIS | env LANG=ja_JP.SJIS ./a.out | iconv -f Shift_JIS
あいうえお
$ echo あいうえお | iconv -t UTF-8 | env LANG=ja_JP.UTF-8 ./a.out | iconv -f UTF-8

Visual C++ 2015でも同じように動きます(試したのはコードページ932でだけです)。

c++ – libc++ vs VC++: Can non-UTF conversions be done with wstring_convert? – Stack Overflowの回答の1つにある、std::codecvt(またはstd::codecvt_byname)の派生クラスを作る方法よりまともな方法が出来上がったと自負しています。

この記事のカテゴリ

  • ⇒ wstring_convertやwbuffer_convertでwchar_tとcharとを変換する

std::wstring_convertとstd::wstring_bufferって、std::localeと組み合わて使うのが難しいです。そんなわけで、その名前とは裏腹にstd::wstring (wchar_t)との組み合わせには使いづらいなあと思っています。

が、しかし、Visual C++はその辺、規格をいい感じにやっちゃって(無視して)いて、使えるようになっていることに気付いてしまいました。

このコード、コンパイルできてしまいました。もちろん、実行すればちゃんと動きます。

#include <codecvt>
#include <locale>
#include <string>
#include <iostream>
 
int main()
{
  using codecvt_wchar = std::codecvt<wchar_t, char, std::mbstate_t>;
 
  std::locale loc("");
  std::wstring_convert<codecvt_wchar> cv(
    &std::use_facet<codecvt_wchar>(loc));
 
  std::string message = "あいうえお";
 
  std::wstring wcs = cv.from_bytes(message);
  std::string mbs = cv.to_bytes(wcs);
 
  std::cout << mbs << std::endl;
}

wbuffer_convertも同様にコンパイルできました。

#include <codecvt>
#include <locale>
#include <string>
#include <iostream>
#include <sstream>
 
int main()
{
  using codecvt_wchar = std::codecvt<wchar_t, char, std::mbstate_t>;
 
  std::locale loc("");
 
  std::wstring_convert<codecvt_wchar> cv(
    &std::use_facet<codecvt_wchar>(loc));
 
  std::wbuffer_convert<codecvt_wchar> buf(
    std::cout.rdbuf(),
    &std::use_facet<codecvt_wchar>(loc));
  std::wostream my_wcout(&buf);
 
  std::wstring wcs = cv.from_bytes("かきくけこ");
  my_wcout << wcs << std::endl;
}

「こういう風に書くとダメなんですよー」という例を出すつもりで書き始めたら、逆にコンパイルエラーが出なくて、驚きました。

  • wstring_convertおよびwstring_bufferのコンストラクタがコンパイルできている。標準規格では、仮引数の型が非constなポインタ型となっているのに、VC++ではconstなポインタ型となっている。use_facetの戻り値の型はconstな参照なのだが、そのせいでコンパイルが通る。
  • wstring_convertおよびwstring_bufferのデストラクタがコンパイルできている。デストラクタでは、コンストラクタで渡したcodecvtへのポインタをdeleteすることになっているのだが、std::codecvtのデストラクタはprotected。VC++のwstring_convertとwbuffer_convertは、内部でstd::localeオブジェクトにcodecvtを持たせることで、この問題を回避している。

GCC (libstdc++)やClang (libc++)だといずれもコンパイルエラーです。

この実装、私がこういう風に使えたら良かったと考えていた感じです。その点では素晴らしいのですが、現実VC++だけが標準規格に沿っていないという現状では、あまり有用に使えないのですよね。

この記事のカテゴリ

  • ⇒ VC++のwstring_convertやwbuffer_convertがちょっと変
  • ⇒ VC++のwstring_convertやwbuffer_convertがちょっと変

GCCやClangでtypeinfo::name()の結果を分かりやすい名前に変換(デマングル)するにあたり、Boostに便利なクラス・関数があります。

まずは、scoped_demangled_nameクラスです。std::type_infoから構築し、getメンバー関数でデマングルした文字列が得られるというものです。

#include <iostream>
#include <typeinfo>
#include <boost/core/demangle.hpp>
 
int main()
{
  // コンストラクタには、const std::type_info&型の仮引数が1つある。
  boost::core::scoped_demangled_name name(typeid(int).name());
  if (name.get())
  {
    std::cout << name.get() << std::endl;
  }
  else
  {
    std::cout << "不明" << std::endl;
  }
}

このほか、std::string型で返すboost::core::demangle関数もあります。こっちは条件判定が要りません。変換失敗時は元の文字列をそのままstd::stringに変換する実装になっています。もちろん、std::stringコンストラクタがstd::bad_allocなどを投げる可能性はあります。

#include <iostream>
#include <typeinfo>
#include <boost/core/demangle.hpp>
 
int main()
{
  // demangle関数もconst std::type_info&型の仮引数が1つある。
  std::cout << boost::core::demangle(typeid(int).name()) << std::endl;
}

内部の実装は、もちろんabi::__cxa_demangleです。

  • RAII化されており、freeを忘れる心配不要(自作するだけでなく、Boostを使うという選択肢が生まれた)。
  • GCCおよびClang以外とのポータビリティが得られる。その他の環境では、実引数で与えられたポインタをそのまま返す実装となっている。そのため、Visual C++などでも、上記のコードがそのまま実行可能。

現在の形になったのはBoost 1.56からです。ドキュメントは最近までありませんでしたが、現在はdemangleにあります。

2016年11月23日追記:Boost.TypeIndexを教えていただきました。より高水準なライブラリとして、こちらを使ってみるのも良さそうです。

この記事のカテゴリ

  • ⇒ Boostで型名をデマングルする

ATL::CAtlExeModuleTを使ったコードを様々なバージョンのVisual C++でコンパイルしていたところ、非互換な変更を見つけました。

問題を見つけたコードとその症状

次のコード、Visual C++のバージョンを上げると動かないという現象に遭遇しました。

#include <iostream>
#include <windows.h>
#include <atlbase.h>
 
#define nullptr NULL
 
class Module : public ATL::CAtlExeModuleT<Module>
{
public:
  HRESULT Run(int showCmd)
  {
    ATL::CComPtr<IUnknown> obj;
    HRESULT hr = CoCreateInstance(CLSID_GlobalOptions, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&obj));
    std::cout << std::showbase << std::hex << hr << std::endl;
    return hr;
  }
 
  int WinMain(int showCmd)
  {
    // 実際には、これ以外にも前処理や後処理がある。
    return static_cast<int>(Run(showCmd));
  }
};
 
Module module;
 
int main()
{
  return module.WinMain(SW_SHOW);
}

Visual C++ 2008までなら、CoCreateInstanceは成功します。ところが、これをVisual C++ 2010以降でコンパイルすると0x800401f0、すなわちCO_E_NOTINITIALIZEDとなってしまいました。

原因・考察

CAtlExeModuleTのソースを見たところ、Visual C++ 2008から2010の間で変更されていることが分かりました。

  • VC++ 2008まで: CAtlExeModuleTのコンストラクタ・デストラクタで、メンバー関数InitializeComとUninitializeComを呼び出している。
  • VC++ 2010から: CAtlExeModuleTのメンバー関数WinMainで、InitializeComとUninitializeComを呼び出している。

以下のようなわけで、この変更は好ましいと考えています。

  • メンバー関数InitializeComとUninitializeComが中で呼び出している、CoInitializeExおよびCoUninitialize関数は、スレッド単位の初期化・終了処理の関数だからです。
  • 最初に提示したコードのように、CAtlExeModuleT(の派生クラス)は、ほとんどの場合グローバル変数として使われます。main関数を実行したスレッド以外でstd::exit関数を呼び出すと、そのスレッドでデストラクタが実行されます。

したがって、Visual C++ 2008までのCAtlExeModuleT実装では、CoInitializeExを実行したことと異なるスレッドでCoUninitializeを実行してしまう可能性がありました。Visual C++ 2010以降は、メンバー関数WinMainにこの処理が移動したので、この問題が解消されたわけです。

修正後のコード

というわけで、上記のコードは次のように修正することとしました。

#include <iostream>
#include <windows.h>
#include <atlbase.h>
 
class Module : public ATL::CAtlExeModuleT<Module>
{
public:
  HRESULT Run(_In_ int nShowCmd)
  {
    ATL::CComPtr<IUnknown> obj;
    HRESULT hr = CoCreateInstance(CLSID_GlobalOptions, nullptr, CLSCTX_INPROC, IID_PPV_ARGS(&obj));
    std::cout << std::showbase << std::hex << hr << std::endl;
    return hr;
  }
 
  int WinMain(int showCmd)
  {
    // 実際には、これ以外にも前処理や後処理がある。
    return ATL::CAtlExeModuleT<Module>::WinMain(showCmd);
  }
};
 
Module module;
 
int main()
{
  return module.WinMain(SW_SHOW);
}

というわけで、CAtlExeModuleTの非互換な変更についての話でした。これ自体は結構前から見つけていて、そのうちブログに書こうと思っていました。前回、それとは無関係にCAtlExeModuleTについて取り上げた(ATL::CAtlExeModuleT::WinMain関数を使う例)ので、その流れでこれを書くことにしたというわけです。

この記事のカテゴリ

  • ⇒ ATL::CAtlExeModuleTのバージョン非互換を見つけた話
  • ⇒ ATL::CAtlExeModuleTのバージョン非互換を見つけた話

ATLのCAtlExeModuleTクラステンプレートには、WinMain関数があります。これを使うウィンドウアプリのサンプルコードを書いてみようと思いました。

以前書いた空のウィンドウを表示するだけのWindowsアプリケーション (WTL)をもとに書いています。

#define UNICODE
#define _UNICODE
 
#define WINVER 0x0501
#define _ATL_XP_TARGETING
#define _ATL_NO_COM_SUPPORT
#define _ATL_APARTMENT_THREADED
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
 
#include <cstdlib>
 
#include <windows.h>
 
#include <atlbase.h>
#include <atltypes.h>
#include <atlwin.h>
 
#include <atlapp.h>
#include <atlcrack.h>
 
class TestWindow : public ATL::CWindowImpl<TestWindow>
{
public:
  DECLARE_WND_CLASS(TEXT("Test Window Class"));
 
private:
  BEGIN_MSG_MAP(TestWindow)
    MSG_WM_DESTROY(OnDestroy)
  END_MSG_MAP()
 
  void OnDestroy()
  {
    PostQuitMessage(0);
  }
};
 
class Module : public ATL::CAtlExeModuleT<Module>
{
public:
  bool ParseCommandLine(_In_ PCTSTR /*cmdLine*/, _Out_ HRESULT* phr)
  {
    *phr = S_OK;
    return true;
  }
 
  HRESULT PreMessageLoop(int cmdShow)
  {
    if (!m_wnd.Create(nullptr, ATL::CWindow::rcDefault,
      TEXT("Hello, world"), WS_OVERLAPPEDWINDOW))
    {
      return HRESULT_FROM_WIN32(GetLastError());
    }
    m_wnd.ShowWindow(cmdShow);
    m_wnd.UpdateWindow();
    return S_OK;
  };
 
private:
  TestWindow m_wnd;
};
 
Module module;
 
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int cmdShow)
{
  std::quick_exit(module.WinMain(cmdShow));
}

CAtlExeModuleT::WinMainは、以下の関数を呼び出します。Run含め、すべてCRTPになっています。つまり、派生クラスでメンバ関数を作ることで、動作をオーバーライドできます。

  1. InitializeCom
  2. ParseCommandLine
  3. Run
    1. PreMessageLoop
    2. RunMessageLoop
    3. PostMessageLoop
  4. UninitializeCom

そのままだと、アウトプロセスのCOMサーバー用のための実装(/RegServerコマンドライン引数を解釈したり、CoRegisterClassObject関数を呼び出したり)が多いです。そのため、不要な処理をある程度取り除いています。具体的には、_ATL_NO_COM_SUPPORTを定義したり、何もしないParseCommandLine関数を定義したりしました。

RunMessageLoop関数がGetMessage関数などによるメッセージループを実装しているため、ソースコードが少し短くなりました。場合によってはアリではないでしょうか。

参考: CAtlExeModuleT クラス(MSDNライブラリ)

この記事のカテゴリ

  • ⇒ ATL::CAtlExeModuleT::WinMain関数を使う例
  • ⇒ ATL::CAtlExeModuleT::WinMain関数を使う例

Boost.勉強会 #21 札幌でLTとして話をしてきました。Visual C++のSAL (source code annotation language)についてです。

このスライドはCreative Commons — 表示 – 継承 4.0 国際 — CC BY-SA 4.0です。ダウンロードはこちらです。

その後、尋ねられた質問の回答を以下書いておきます。

こういう低水準な処理はラップして隠蔽するものでは?

はい、そう思います。実際、C++プログラムにおいて、SALを適用する箇所は少ない実感があります。

Visual C++以外でも使用する予定のコードがある。空に置き換えるマクロ定義が用意されていないか?

無さそうです。実際、C++ REST SDKでは、cpprest/details/nosal.hにて自前で作っています。このように自身で作るしかないと思います。

_In_reads_(n)など配列の場合で、要素数が変数であるのにコンパイル時に範囲外アクセスを検出できるのか?

はい、可能なものもあると思います。MSDNライブラリのSAL についてというページには、こういう例があります。

wchar_t * wmemcpy(
  _Out_writes_all_(count) wchar_t *dest, 
  _In_reads_(count) const wchar_t *src, 
  size_t count)
{
  size_t i;
  for (i = 0; i <= count; i++) { // BUG: off-by-one error
    dest[i] = src[i];
  }
  return dest;
}

src[count - 1]までしか読み取れないはずなのに、iは最大countに達する可能性があります。

機械的に検出できそうな感じがするでしょう。でもこれ、Visual C++の/analyzeでは何も言われないという残念なオチでした: c++ – How Microsoft SAL can prevent off-by-one error – Stack Overflow。すみません、私もこれを書いているたった今知りました。


そんなわけで、抜けもありますが、自分はこれでバグを見つけてもらったことが何度かあるので、SALを自分の書くコードにも使っています。ここ最近このブログに載せているコードも全部そうです。今回紹介しきれなかったものも、今後紹介していきたいと思っています。

この記事のカテゴリ

  • ⇒ Visual C++コード分析を支えるSALという題で話をしてきました

WebBrowserコントロールの機能を制限する話、これで最後です。

今回は、これまでやっていなかったこと、まとめてコードを書きました。

  • ページ遷移の禁止: DWebBrowserEvents2のBeforeNavigate2イベントを受信しての処理
  • コンテキストメニュー(右クリックなど)の禁止: IAxWinAmbientDispatchのput_AllowContextMenu
  • その他: IAxWinAmbientDispatchのput_AllowShowUI、IWebBrowser2のput_RegisterAsBrowserとput_RegisterAsDropTargetとput_Silent

ソースコードを載せます。

#define UNICODE
#define _UNICODE
 
#define WINVER 0x0501
#define _ATL_XP_TARGETING
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
 
#include <cstdlib>
 
#include <windows.h>
#include <exdisp.h>
#include <exdispid.h>
 
#include <atlbase.h>
#include <atltypes.h>
#include <atlwin.h>
 
#include <atlapp.h>
#include <atlcrack.h>
 
class Module : public ATL::CAtlExeModuleT<Module>
{
};
 
Module module;
 
constexpr UINT ID_WEBBROWSER_EVENTS = 1;
 
class TestWindow :
  public ATL::CWindowImpl<TestWindow>,
  public WTL::CMessageFilter,
  public ATL::IDispEventImpl<ID_WEBBROWSER_EVENTS, TestWindow, &__uuidof(DWebBrowserEvents2), &LIBID_SHDocVw, 1, 1>
{
public:
  int Run(int cmdShow, _Inout_ WTL::CMessageLoop& msgLoop)
  {
    if (!Create(nullptr, ATL::CWindow::rcDefault,
      TEXT("Hello, world"), WS_OVERLAPPEDWINDOW))
    {
      return -1;
    }
 
    ShowWindow(cmdShow);
    UpdateWindow();
 
    msgLoop.AddMessageFilter(this);
    return msgLoop.Run();
  }
 
  BOOL PreTranslateMessage(_In_ MSG* pmsg) override
  {
    if (m_webBrowser.IsWindow()
      && m_webBrowser.SendMessage(
        WM_FORWARDMSG, 0, reinterpret_cast<LPARAM>(pmsg)))
    {
      return TRUE;
    }
    return IsDialogMessage(pmsg);
  }
 
  DECLARE_WND_CLASS(TEXT("Test Window Class"));
 
  BEGIN_MSG_MAP(TestWindow)
    MSG_WM_SIZE(OnSize)
    MSG_WM_SETFOCUS(OnSetFocus)
    MSG_WM_CREATE(OnCreate)
    MSG_WM_DESTROY(OnDestroy)
  END_MSG_MAP()
 
  BEGIN_SINK_MAP(TestWindow)
    SINK_ENTRY_EX(ID_WEBBROWSER_EVENTS, __uuidof(DWebBrowserEvents2), DISPID_BEFORENAVIGATE2, &TestWindow::BeforeNavigate2)
  END_SINK_MAP()
 
private:
  void __stdcall BeforeNavigate2(
    _In_ IDispatch* /*disp*/,
    _In_ VARIANT* /*url*/,
    _In_ VARIANT* /*flags*/,
    _In_ VARIANT* /*targetFrameName*/,
    _In_ VARIANT* /*postData*/,
    _In_ VARIANT* /*headers*/,
    _Inout_ VARIANT_BOOL* cancel)
  {
    if (cancel != nullptr)
    {
      *cancel = VARIANT_TRUE;
    }
  }
 
  void OnSize(UINT, SIZE size)
  {
    m_webBrowser.ResizeClient(size.cx, size.cy);
  }
 
  void OnSetFocus(HWND)
  {
    m_webBrowser.SetFocus();
  }
 
  LRESULT OnCreate(const CREATESTRUCT*)
  {
    constexpr DWORD IDC_WEBBROWSER_CONTROL = 1;
    RECT empty{};
    m_webBrowser.Create(m_hWnd, empty, L"https://www.google.co.jp/",
      WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | WS_GROUP | WS_VSCROLL | WS_HSCROLL, 0,
      IDC_WEBBROWSER_CONTROL);
 
    ATL::CComPtr<IAxWinAmbientDispatch> ambient;
    m_webBrowser.QueryHost(&ambient);
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(ambient->put_AllowContextMenu(VARIANT_FALSE)),
      -1);
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(ambient->put_AllowShowUI(VARIANT_FALSE)),
      -1);
 
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(InitializeWebBrowserControl()),
      -1);
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(ATL::AtlAdviseSinkMap(this, true)),
      -1);
 
    return 0;
  }
 
  HRESULT InitializeWebBrowserControl()
  {
    ATL::CComPtr<IWebBrowser2> wb;
    if (SUCCEEDED(m_webBrowser.QueryControl(&wb)))
    {
      auto hrRB = wb->put_RegisterAsBrowser(VARIANT_FALSE);
      ATLENSURE_RETURN_HR(SUCCEEDED(hrRB), hrRB);
      auto hrRDT = wb->put_RegisterAsDropTarget(VARIANT_TRUE);
      ATLENSURE_RETURN_HR(SUCCEEDED(hrRDT), hrRDT);
      auto hrSl = wb->put_Silent(VARIANT_TRUE);
      ATLENSURE_RETURN_HR(SUCCEEDED(hrSl), hrSl);
    }
    return S_OK;
  }
 
  void OnDestroy()
  {
    PostQuitMessage(0);
  }
 
  ATL::CAxWindow m_webBrowser;
};
 
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int cmdShow)
{
  if (FAILED(OleInitialize(nullptr)))
  {
    std::quick_exit(-1);
  }
 
  WTL::CMessageLoop msgLoop;
  TestWindow wnd;
  std::quick_exit(wnd.Run(cmdShow, msgLoop));
}

コンテキストメニュー(いわゆる右クリックメニュー)の禁止は、本来IDocHostUIHandlerのShowContextMenuで制御します。ところが、ATL::CAxHostWindowがIDocHostUIHandlerを実装していて、ATL::CAxWindowを使う側ではAllowContextMenuプロパティで簡単に制御できるようになっています。AllowShowUIも同じです。

DWebBrowserEvents2のイベントの受信のコードは、BEGIN_SINK_MAPやAtlAdviseSinkMap関数などをオーソドックスに使って書き上げています。BeforeNavigate2で、cancelにVARIANT_TRUEを書き込むことで、リンクをクリックしてもページ遷移を実行させないようにしています。

ここまで、単なるHTMLビューアーとしてWebBroserコントロールを使いたいと思って、いろいろ試してきました。これでだいたいやり尽くしたのではないかと考えています。

この記事のカテゴリ

  • ⇒ WebBrowserコントロールでコンテキストメニューとページ遷移とその他制限を加える
  • ⇒ WebBrowserコントロールでコンテキストメニューとページ遷移とその他制限を加える
  • ⇒ WebBrowserコントロールでコンテキストメニューとページ遷移とその他制限を加える

前回(WebBrowserコントロールでJavaScriptほか色々禁止する (3) DISPID_AMBIENT_DLCONTROLその1)の続きです。ATLのDLLが廃止されたVisual C++ 2013以降を使いつつ、ATL::CAxWindowでDISPID_AMBIENT_DLCONTROLやその他のアンビエントプロパティを使う方法です。


分かってしまえば簡単なことでした。IAxWinAmbientDispatchExを自身のタイプライブラリに含めてしまえば良いのです。というわけで、早速コードを書いておきます。

まずは、以下のようなIDLファイルを作ります。

import "atliface.idl";
 
[
  uuid(c595583a-0d92-4490-94fe-42e3ab446071)
]
library Test
{
  interface IAxWinAmbientDispatchEx;
}

これをmidl Test.idlとしてコンパイルし、Test.tlbを得ます。

次にリソーススクリプトを作ります。

1 TYPELIB "Test.tlb"

こちらもrc Test.rcとしてコンパイルします。

そうしたら、次はソースコードです。前回のものと比べ、_ATL_DLLとquick_exitとconstexprのdefineを削除しただけです。Visual C++ 2015にはquick_exitもconstexprもあります。

#define UNICODE
#define _UNICODE
 
#define WINVER 0x0501
#define _ATL_XP_TARGETING
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
 
#include <cstdlib>
 
#include <windows.h>
#include <mshtmdid.h>
 
#include <atlbase.h>
#include <atltypes.h>
#include <atlwin.h>
 
#include <atlapp.h>
#include <atlcrack.h>
 
#include <boost/implicit_cast.hpp>
 
class Module : public ATL::CAtlExeModuleT<Module>
{
};
 
Module module;
 
constexpr UINT ID_WEBBROWSER_AMBIENT = 0;
 
class WebBrowserAmbient :
  public ATL::IDispEventSimpleImpl<ID_WEBBROWSER_AMBIENT, WebBrowserAmbient, &IID_NULL>
{
  static ATL::_ATL_FUNC_INFO* GetDlControlInfo()
  {
    static ATL::_ATL_FUNC_INFO GetDlControlInfo = { CC_STDCALL, VT_I4, 0, {} };
    return &GetDlControlInfo;
  }
 
public:
  BEGIN_SINK_MAP(WebBrowserAmbient)
    SINK_ENTRY_INFO(ID_WEBBROWSER_AMBIENT, IID_NULL, DISPID_AMBIENT_DLCONTROL, &WebBrowserAmbient::GetDlControl, GetDlControlInfo())
  END_SINK_MAP()
 
public:
  int __stdcall GetDlControl()
  {
    return DLCTL_NO_SCRIPTS
      | DLCTL_NO_JAVA
      | DLCTL_NO_RUNACTIVEXCTLS
      | DLCTL_NO_DLACTIVEXCTLS
      | DLCTL_DOWNLOADONLY
      | DLCTL_NO_FRAMEDOWNLOAD
      | DLCTL_NO_BEHAVIORS
      | DLCTL_NOFRAMES
      | DLCTL_SILENT;
  }
 
  IDispatch* GetDispatch()
  {
    return reinterpret_cast<IDispatch*>(
      boost::implicit_cast<IDispEventSimpleImpl*>(this));
  }
};
 
class TestWindow :
  public ATL::CWindowImpl<TestWindow>,
  public WTL::CMessageFilter
{
public:
  int Run(int cmdShow, _Inout_ WTL::CMessageLoop& msgLoop)
  {
    if (!Create(nullptr, ATL::CWindow::rcDefault,
      TEXT("Hello, world"), WS_OVERLAPPEDWINDOW))
    {
      return -1;
    }
 
    ShowWindow(cmdShow);
    UpdateWindow();
 
    msgLoop.AddMessageFilter(this);
    return msgLoop.Run();
  }
 
  BOOL PreTranslateMessage(_In_ MSG* pmsg) override
  {
    if (m_webBrowser.IsWindow()
      && m_webBrowser.SendMessage(
        WM_FORWARDMSG, 0, reinterpret_cast<LPARAM>(pmsg)))
    {
      return TRUE;
    }
    return IsDialogMessage(pmsg);
  }
 
  DECLARE_WND_CLASS(TEXT("Test Window Class"));
 
  BEGIN_MSG_MAP(TestWindow)
    MSG_WM_SIZE(OnSize)
    MSG_WM_SETFOCUS(OnSetFocus)
    MSG_WM_CREATE(OnCreate)
    MSG_WM_DESTROY(OnDestroy)
  END_MSG_MAP()
 
private:
  void OnSize(UINT, SIZE size)
  {
    m_webBrowser.ResizeClient(size.cx, size.cy);
  }
 
  void OnSetFocus(HWND)
  {
    m_webBrowser.SetFocus();
  }
 
  LRESULT OnCreate(const CREATESTRUCT*)
  {
    constexpr DWORD IDC_WEBBROWSER_CONTROL = 1;
    RECT empty = {};
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(m_webBrowser.Create(m_hWnd, empty, L"",
        WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | WS_GROUP, 0,
        IDC_WEBBROWSER_CONTROL)),
      -1);
 
    ATL::CComPtr<IAxWinAmbientDispatchEx> ambientDispatch;
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(m_webBrowser.QueryHost(&ambientDispatch)),
      -1);
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(ambientDispatch->SetAmbientDispatch(m_ambient.GetDispatch())),
      -1);
 
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(m_webBrowser.CreateControl(L"http://enable-javascript.com/ja/")),
      -1);
 
    return 0;
  }
 
  void OnDestroy()
  {
    PostQuitMessage(0);
  }
 
  WebBrowserAmbient m_ambient;
  ATL::CAxWindow m_webBrowser;
};
 
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int cmdShow)
{
  if (FAILED(OleInitialize(nullptr)))
  {
    std::quick_exit(-1);
  }
 
  WTL::CMessageLoop msgLoop;
  TestWindow wnd;
  std::quick_exit(wnd.Run(cmdShow, msgLoop));
}

これをcl test.cpp test.resなど、先ほど作ったリソースを含めてコンパイルします。すると、前回のVisual C++ 2012対象に書いたコードと同じように動作します。


これを明らかになるまで調べた結果を簡潔に書きます。

ATL::CAxHostWindowは、基底クラスにIDispatchImpl<IAxWinAmbientDispatchEx, &__uuidof(IAxWinAmbientDispatchEx), &CAtlModule::m_libid, 0xFFFF, 0xFFFF>があります。また、ATL::CAxHostWindow::Invokeが以下のように実装されています。

STDMETHOD(Invoke)(
	_In_ DISPID dispIdMember,
	_In_ REFIID riid,
	_In_ LCID lcid,
	_In_ WORD wFlags,
	_In_ DISPPARAMS *pDispParams,
	_Out_opt_ VARIANT *pVarResult,
	_Out_opt_ EXCEPINFO *pExcepInfo,
	_Out_opt_ UINT *puArgErr)
{
	HRESULT hr = IDispatchImpl<IAxWinAmbientDispatchEx, &__uuidof(IAxWinAmbientDispatchEx), &CAtlModule::m_libid, 0xFFFF, 0xFFFF>::Invoke
		(dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
	if ((hr == DISP_E_MEMBERNOTFOUND || hr == TYPE_E_ELEMENTNOTFOUND) && m_spAmbientDispatch != NULL)
	{
		hr = m_spAmbientDispatch->Invoke(dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
		if (SUCCEEDED(hr) && (wFlags & DISPATCH_PROPERTYPUT) != 0)
		{
			hr = FireAmbientPropertyChange(dispIdMember);
		}
	}
	return hr;
}

m_spAmbientDispatchに、SetAmbientDispatchで渡したオブジェクトが入っています。したがって、IDispatchImplのInvokeがDISP_E_MEMBERNOTFOUNDかTYPE_E_ELEMENTNOTFOUNDを返さないと、m_spAmbientDispatchが使われることはないのです。

タイプライブラリがリソースに入っていなかったり、タイプライブラリはあるけどIAxWinAmbientDispatchExが入っていなかったりする場合、エラーコードはそのどちらでもないので、うまくいきません。そのため、IAxWinAmbientDispatchExを含むタイプライブラリを含めるという方法に辿り着きました。


DISPID_AMBIENT_DLCONTROLを使う話、無事終わりました。なお、ウェブブラウザコントロールの話はまだ続きます。

この記事のカテゴリ

  • ⇒ WebBrowserコントロールでJavaScriptほか色々禁止する (4) DISPID_AMBIENT_DLCONTROLその2
  • ⇒ WebBrowserコントロールでJavaScriptほか色々禁止する (4) DISPID_AMBIENT_DLCONTROLその2
  • ⇒ WebBrowserコントロールでJavaScriptほか色々禁止する (4) DISPID_AMBIENT_DLCONTROLその2

引き続き、ATLでWebBrowserコントロールで使用する場合にJavaScriptその他を禁止する方法の話です。3回目からは、ちょっと違うやり方、DISPID_AMBIENT_DLCONTROLです。

インターネット上で検索すると、DISPID_AMBIENT_DLCONTROLを使うやり方のほうが多く見受けられるのですが、ちょっと問題があって後回しにしていました。その理由は、「Visual C++ 2013以降だと、ATL::CAxWindowと組み合わせての利用が少し困難だから」です。

というわけで、今回のプログラムは、Visual C++ 2012で動作確認しました。

#define UNICODE
#define _UNICODE
 
#define WINVER 0x0501
#define _ATL_DLL
#define _ATL_XP_TARGETING
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
 
#include <cstdlib>
 
#include <windows.h>
#include <mshtmdid.h>
 
#include <atlbase.h>
#include <atltypes.h>
#include <atlwin.h>
 
#include <atlapp.h>
#include <atlcrack.h>
 
#include <boost/implicit_cast.hpp>
 
#define quick_exit exit
#define constexpr const
 
class Module : public ATL::CAtlExeModuleT<Module>
{
};
 
Module module;
 
constexpr UINT ID_WEBBROWSER_EVENTS = 1;
 
class WebBrowserAmbient :
  public ATL::IDispEventSimpleImpl<ID_WEBBROWSER_AMBIENT, WebBrowserAmbient, &IID_NULL>
{
  static ATL::_ATL_FUNC_INFO* GetDlControlInfo()
  {
    static ATL::_ATL_FUNC_INFO GetDlControlInfo = { CC_STDCALL, VT_I4, 0, {} };
    return &GetDlControlInfo;
  }
 
public:
  BEGIN_SINK_MAP(WebBrowserAmbient)
    SINK_ENTRY_INFO(ID_WEBBROWSER_AMBIENT, IID_NULL, DISPID_AMBIENT_DLCONTROL, &WebBrowserAmbient::GetDlControl, GetDlControlInfo())
  END_SINK_MAP()
 
public:
  int __stdcall GetDlControl()
  {
    return DLCTL_NO_SCRIPTS
      | DLCTL_NO_JAVA
      | DLCTL_NO_RUNACTIVEXCTLS
      | DLCTL_NO_DLACTIVEXCTLS
      | DLCTL_DOWNLOADONLY
      | DLCTL_NO_FRAMEDOWNLOAD
      | DLCTL_NO_BEHAVIORS
      | DLCTL_NOFRAMES
      | DLCTL_SILENT;
  }
 
  IDispatch* GetDispatch()
  {
    return reinterpret_cast<IDispatch*>(
      boost::implicit_cast<IDispEventSimpleImpl*>(this));
  }
};
 
class TestWindow :
  public ATL::CWindowImpl<TestWindow>,
  public WTL::CMessageFilter
{
public:
  int Run(int cmdShow, _Inout_ WTL::CMessageLoop& msgLoop)
  {
    if (!Create(nullptr, ATL::CWindow::rcDefault,
      TEXT("Hello, world"), WS_OVERLAPPEDWINDOW))
    {
      return -1;
    }
 
    ShowWindow(cmdShow);
    UpdateWindow();
 
    msgLoop.AddMessageFilter(this);
    return msgLoop.Run();
  }
 
  BOOL PreTranslateMessage(_In_ MSG* pmsg) override
  {
    if (m_webBrowser.IsWindow()
      && m_webBrowser.SendMessage(
        WM_FORWARDMSG, 0, reinterpret_cast<LPARAM>(pmsg)))
    {
      return TRUE;
    }
    return IsDialogMessage(pmsg);
  }
 
  DECLARE_WND_CLASS(TEXT("Test Window Class"));
 
  BEGIN_MSG_MAP(TestWindow)
    MSG_WM_SIZE(OnSize)
    MSG_WM_SETFOCUS(OnSetFocus)
    MSG_WM_CREATE(OnCreate)
    MSG_WM_DESTROY(OnDestroy)
  END_MSG_MAP()
 
private:
  void OnSize(UINT, SIZE size)
  {
    m_webBrowser.ResizeClient(size.cx, size.cy);
  }
 
  void OnSetFocus(HWND)
  {
    m_webBrowser.SetFocus();
  }
 
  LRESULT OnCreate(const CREATESTRUCT*)
  {
    constexpr DWORD IDC_WEBBROWSER_CONTROL = 1;
    RECT empty = {};
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(m_webBrowser.Create(m_hWnd, empty, L"",
        WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | WS_GROUP, 0,
        IDC_WEBBROWSER_CONTROL)),
      -1);
 
    ATL::CComPtr<IAxWinAmbientDispatchEx> ambientDispatch;
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(m_webBrowser.QueryHost(&ambientDispatch)),
      -1);
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(ambientDispatch->SetAmbientDispatch(m_ambient.GetDispatch())),
      -1);
 
    ATLENSURE_RETURN_VAL(
      SUCCEEDED(m_webBrowser.CreateControl(L"http://enable-javascript.com/ja/")),
      -1);
 
    return 0;
  }
 
  void OnDestroy()
  {
    PostQuitMessage(0);
  }
 
  WebBrowserAmbient m_ambient;
  ATL::CAxWindow m_webBrowser;
};
 
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int cmdShow)
{
  if (FAILED(OleInitialize(nullptr)))
  {
    std::quick_exit(-1);
  }
 
  WTL::CMessageLoop msgLoop;
  TestWindow wnd;
  std::quick_exit(wnd.Run(cmdShow, msgLoop));
}

このプログラムは、次のようなことをやっています。

一番大事なのがWebBrowserAmbientクラスのGetDlControl関数で、ID_WEBBROWSER_AMBIENTのgetに対する処理を書いているところです。ここを変えればWebBrowserコントロールの挙動が変化します。


ここから先は、atl110.dllを使うようにした理由の話です。

ActiveXコントロールからのアンビエントプロパティの取得を受け付けるのはATL::CAxHostWindowです。このクラスのInvoke関数の実装を読みました。すると、SetAmbientDispatchで設定したIDispatchオブジェクトに処理が渡されるには、ちょっとした条件を満たしている必要があることが分かりました。その条件とは、

  • モジュール自身のリソースとしてタイプライブラリがある。
  • そのタイプライブラリの中にIAxWinAmbientDispatchExの情報が含まれている。

atl110.dllには、ATL::CAxHostWindowの実装が含まれており、しかもこの条件を満たしています。そのため、atl110.dllを使うのが簡単だと考えました。

今回、Visual C++ 2012を使った理由もそこにあります。Visual C++ 2013以降、ATLのDLLは廃止されたので、この方法は使えなくなりました: MSDNライブラリのATL アプリケーションの再配布


次回は、VC++ 2013以降でATL::CAxHostWindowを使いつつ、SetAmbientDispatchを使う方法のコードを載せます。

この記事のカテゴリ

  • ⇒ WebBrowserコントロールでJavaScriptほか色々禁止する (3) DISPID_AMBIENT_DLCONTROLその1
  • ⇒ WebBrowserコントロールでJavaScriptほか色々禁止する (3) DISPID_AMBIENT_DLCONTROLその1
  • ⇒ WebBrowserコントロールでJavaScriptほか色々禁止する (3) DISPID_AMBIENT_DLCONTROLその1

次ページへ »