「MFCコントロール - 分割ウインドウ」の版間の差分
編集の要約なし |
|||
| (3人の利用者による、間の13版が非表示) | |||
| 1行目: | 1行目: | ||
== 概要 == | == 概要 == | ||
分割ウィンドウとは、ひとつのフレームウィンドウのクライアント領域を複数のペインに分割して表示するウィンドウ構成である。<br> | |||
Microsoft Foundation Classes (MFC) では、<code>CSplitterWnd</code> クラスを使用してこの機能を実現できる。<br> | |||
<br> | |||
各ペインは通常、異なる <code>CView</code> 派生クラス、または 同じ <code>CView</code> 派生クラスのインスタンスを割り当てる。<br> | |||
<br> | |||
MFCの分割ウィンドウには、静的スプリッタと動的スプリッタの2種類が存在する。<br> | |||
<br> | |||
静的スプリッタは <code>CreateStatic()</code> を使用して、作成時に決めた行数と列数のペインを全て生成する。<br> | |||
ユーザはペインのサイズを変更できるが、ペインの数や順序は変更できない。<br> | |||
<br. | |||
動的スプリッタは <code>Create()</code> を使用して、初期状態では1つのペインから始まる。<br> | |||
ユーザがスプリッタボックスを操作すると、フレームワークが新しいペインを動的に作成・破棄する。<br> | |||
<br> | |||
静的スプリッタは異なるビュークラスを組み合わせて、複数の情報を同時に表示する用途に適している。<br> | |||
動的スプリッタは、同じドキュメントを複数の視点やスクロール位置で確認する用途に適している。<br> | |||
<br> | |||
<code>CSplitterWnd</code> クラスは <code>CFrameWnd</code> または <code>CMDIChildWnd</code> の派生クラスに埋め込んで使用する。<br> | |||
<br> | |||
実装の中核は、フレームクラスの <code>OnCreateClient()</code> をオーバーライドして、その中で <code>CreateStatic()</code> または <code>Create()</code> を呼び出すことである。<br> | |||
その後、静的スプリッタの場合は <code>CreateView()</code> を使用して、各ペインにビューを割り当てる必要がある。<br> | |||
<br> | |||
分割ウィンドウは、ドキュメント/ビューアーキテクチャと組み合わせることで、複雑なユーザインターフェースを比較的少ないコードで構築できる。<br> | |||
<br> | |||
<u>ただし、MFCは引き続きサポートされているが、新機能の追加やドキュメントの更新は行われていないことに注意すること。</u><br> | |||
<br><br> | <br><br> | ||
== | == 前提条件 == | ||
分割ウィンドウを理解するためには、以下に示す知識と環境が必要である。<br> | |||
<br> | |||
* C++によるデスクトップ開発ワークロードを有効にする。 | |||
* ドキュメント / ビューアーキテクチャの基礎知識 | |||
*: <code>CWinApp</code>、<code>CDocument</code>、<code>CView</code>、<code>CFrameWnd</code> の関係を理解していること。 | |||
* C++とWindows APIの知識 | |||
*: ポインタ、参照、メッセージハンドラ、<code>CRuntimeClass</code> クラス等を理解していること。 | |||
* <code>DECLARE_DYNCREATE</code> / <code>IMPLEMENT_DYNCREATE</code> マクロの理解 | |||
*: 分割ウィンドウの各ペインに割り当てるViewクラスは、動的生成が可能である必要がある。 | |||
<br><br> | |||
== 分割ウィンドウの種類 == | |||
MFCで提供される分割ウィンドウには、静的スプリッタと動的スプリッタの2種類がある。<br> | |||
用途に応じて適切な方を選択すること。<br> | |||
<br> | |||
<center> | |||
{| class="wikitable" | |||
|+ 静的スプリッタと動的スプリッタの比較 | |||
! 項目 !! 静的スプリッタ (<code>CreateStatic</code>) !! 動的スプリッタ (<code>Create</code>) | |||
|- | |||
| ペイン数 || 固定 || ユーザ操作で増減 | |||
|- | |||
| 最大分割数 || 16行 x 16列 || 2行 x 2列 | |||
|- | |||
| ペインのクラス || 異なる <code>CView</code> 派生クラスを割り当て可能 || 基本的に同じ <code>CView</code> 派生クラス | |||
|- | |||
| スクロールバー || 共有スクロールバーを <code>WS_HSCROLL</code> / <code>WS_VSCROLL</code> で追加可能 || 動的に生成されるペインと連動 | |||
|- | |||
| 使用例 || Windowsエクスプローラー<br>Visual Studioのグラフィックスエディタ || Microsoft Excel<br>Microsoft Word | |||
|- | |||
| 主な用途 || 異なる情報を同じウィンドウ内に同時表示 || 同じドキュメントを複数視点で確認 | |||
|} | |||
</center> | |||
<br> | |||
静的スプリッタを使用する場合、全てのペインを <code>OnCreateClient()</code> 内で明示的に作成する必要がある。<br> | |||
<br> | |||
動的スプリッタを使用する場合、フレームワークがペインの作成と破棄を自動的に行う。<br> | |||
<br><br> | |||
== 基本的な実装手順 == | |||
分割ウィンドウを実装する時の流れを以下に示す。<br> | |||
<br> | |||
# フレームクラスに <code>CSplitterWnd</code> クラスのメンバ変数を追加する。 | |||
#: SDIアプリケーションの場合は、<code>CMainFrame</code> クラス、MDIアプリケーションの場合は、<code>CChildFrame</code> クラスに追加する。 | |||
# フレームクラスの <code>OnCreateClient()</code> をオーバーライドする。 | |||
#: クラスビューから <code>OnCreateClient()</code> を追加する、または、ヘッダファイルで手動で宣言する。 | |||
# <code>OnCreateClient()</code> 内で <code>CreateStatic()</code> または <code>Create()</code> を呼び出す。 | |||
#: 静的スプリッタの場合は <code>CreateStatic()</code>、動的スプリッタの場合は <code>Create()</code> を使用する。 | |||
# 静的スプリッタの場合、全てのペインに <code>CreateView()</code> でビューを割り当てる。 | |||
#: 各ペインに対応する <code>CView</code> 派生クラスを <code>RUNTIME_CLASS</code> マクロで指定する。 | |||
# 必要に応じて、<code>SetRowInfo()</code> / <code>SetColumnInfo()</code> でサイズを調整する。 | |||
#: リサイズ時には、<code>OnSize()</code> ハンドラで再調整することもある。 | |||
<br> | |||
下図は、ドキュメント / ビューアーキテクチャにおける分割ウィンドウの位置づけを示している。<br> | |||
<br> | |||
[[ファイル:MFC Splitter Architecture.png|フレームなし|中央]] | |||
<br><br> | |||
== 静的分割ウィンドウ == | |||
静的分割ウィンドウは、<code>CreateStatic()</code> を使用して作成する。<br> | |||
<br> | |||
ユーザはペインの境界線をドラッグしてサイズを変更できるが、ペインの数は増減しない。<br> | |||
<br> | |||
==== 左右2分割の作成 ==== | |||
1行2列の分割ウィンドウを作成する例を示す。<br> | |||
<br> | |||
左側のペイン (Pane A) にはプロジェクト作成時に生成されたViewクラスを使用し、右側のペイン (Pane B) には新規に作成したViewクラスを使用する。<br> | |||
左側がPaneA (CPaneAView)、右側がPaneB (CPaneBView) となっている。<br> | |||
<br> | |||
[[ファイル:MFC Splitter 2Pane.png|フレームなし|中央]] | |||
<br> | |||
[[ファイル:MFC Pane 01.jpg|フレームなし|中央]] | [[ファイル:MFC Pane 01.jpg|フレームなし|中央]] | ||
<br> | <br> | ||
まず、右ペイン用のViewクラスを追加する。<br> | |||
ソリューションエクスプローラーでプロジェクトを右クリックして、[追加] - [クラス] を選択する。<br> | |||
<br> | |||
* クラスの種類 | |||
*: MFCクラス | |||
* クラス名 | |||
*: <u>CPaneBView</u> | |||
* 基本クラス | |||
*: <u>CFormView</u> または <u>CView</u> | |||
<br> | <br> | ||
次に、<u>CMainFrame.h</u> に <code>CSplitterWnd</code> のメンバ変数を追加する。<br> | |||
MDIアプリケーションの場合は <u>CChildFrame.h</u> に同様の変更を行う。<br> | |||
<br> | <br> | ||
<syntaxhighlight lang="c++"> | |||
// CMainFrame.h | |||
< | |||
CMainFrame.h | |||
class CMainFrame : public CFrameWndEx | class CMainFrame : public CFrameWndEx | ||
{ | { | ||
// ... | // ... | ||
public: | |||
CSplitterWnd m_SplitMain; // 分割ウィンドウ | |||
// ... | // ... | ||
}; | |||
</syntaxhighlight> | |||
<br> | |||
次に、<u>CMainFrame.cpp</u> の <code>OnCreateClient()</code> を以下に示すように実装する。<br> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// CMainFrame.cpp | |||
#include "CPaneDoc.h" | |||
#include "CPaneAView.h" | |||
#include "CPaneBView.h" | |||
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) | |||
{ | |||
// 1行2列の静的スプリッタを作成 | |||
if (!m_SplitMain.CreateStatic(this, 1, 2)) | |||
{ | |||
TRACE0("Failed to create splitter window\n"); | |||
return FALSE; | |||
} | |||
// ペインA (row=0, col=0) | |||
if (!m_SplitMain.CreateView(0, 0, RUNTIME_CLASS(CPaneAView), CSize(200, 0), pContext)) | |||
{ | |||
TRACE0("Failed to create pane A\n"); | |||
return FALSE; | |||
} | |||
// ペインB (row=0, col=1) | |||
if (!m_SplitMain.CreateView(0, 1, RUNTIME_CLASS(CPaneBView), CSize(0, 0), pContext)) | |||
{ | |||
TRACE0("Failed to create pane B\n"); | |||
return FALSE; | |||
} | |||
return TRUE; | |||
} | } | ||
</ | </syntaxhighlight> | ||
<br> | |||
<code>CreateStatic()</code> の第2引数は行数、第3引数は列数である。<br> | |||
<br> | <br> | ||
<code>CreateView()</code> の第1引数と第2引数は、それぞれ行番号と列番号を0始まりで指定する。<br> | |||
第3引数には、ペインを管理する <code>CView</code> 派生クラスの <code>CRuntimeClass</code> クラスを <code>RUNTIME_CLASS</code> マクロで指定する。<br> | |||
第4引数の <code>CSize</code> クラスは、理想サイズを示す。<br> | |||
第5引数の <code>pContext</code> は、親フレームから渡された作成コンテキストをそのまま渡す。<br> | |||
<br> | <br> | ||
==== ネストした3分割の作成 ==== | |||
< | <code>CreateStatic()</code> は、簡単な <u>n行 x m列</u> のグリッドしか直接作成できない。<br> | ||
<br> | |||
例えば、左側に1つ、右側に上下2つという変則的なレイアウトを作成する場合は、2つの <code>CSplitterWnd</code> クラスをネストする。<br> | |||
具体的には、メインフレームを左右に分割した後、右側のペインをさらに上下に分割する。<br> | |||
<br> | |||
[[ファイル:MFC Splitter 3Pane Nested.png|フレームなし|中央]] | |||
<br> | |||
まず、Pane A、Pane B、Pane C向けのViewクラスをそれぞれ作成する。<br> | |||
<br> | |||
<u>CMainFrame.h</u> には、メインスプリッタとサブスプリッタの2つのメンバ変数を追加する。<br> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// CMainFrame.h | |||
class CMainFrame : public CFrameWndEx | |||
{ | |||
// ... | |||
public: | |||
CSplitterWnd m_SplitMain; // メインフレームを左右分割 | |||
CSplitterWnd m_SplitSub; // 右ペインを上下分割 | |||
// ... | |||
}; | |||
</syntaxhighlight> | |||
<br> | |||
次に、<u>CMainFrame.cpp</u> の <code>OnCreateClient()</code> を以下に示すように定義する。<br> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// CMainFrame.cpp | |||
#include "CPaneDoc.h" | |||
#include "CPaneAView.h" | |||
#include "CPaneBView.h" | |||
#include "CPaneCView.h" | |||
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) | BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) | ||
{ | { | ||
// | // メインスプリッタを左右2分割 | ||
if( | if (!m_SplitMain.CreateStatic(this, 1, 2)) | ||
{ | |||
TRACE0("Failed to create main splitter\n"); | |||
return FALSE; | |||
} | |||
// サブスプリッタを右ペインに上下2分割 | |||
if (!m_SplitSub.CreateStatic(&m_SplitMain, 2, 1, WS_CHILD | WS_VISIBLE | WS_BORDER, m_SplitMain.IdFromRowCol(0, 1))) | |||
{ | |||
TRACE0("Failed to create sub splitter\n"); | |||
return FALSE; | |||
} | |||
// ペインA (左側) | |||
if (!m_SplitMain.CreateView(0, 0, RUNTIME_CLASS(CPaneAView), CSize(320, 240), pContext)) | |||
{ | { | ||
TRACE0("Failed to create pane A\n"); | |||
return FALSE; | return FALSE; | ||
} | } | ||
if( | // ペインB (右側上部) | ||
if (!m_SplitSub.CreateView(0, 0, RUNTIME_CLASS(CPaneBView), CSize(320, 120), pContext)) | |||
{ | { | ||
TRACE0("Failed to create pane B\n"); | |||
return FALSE; | return FALSE; | ||
} | } | ||
if( | // ペインC (右側下部) | ||
if (!m_SplitSub.CreateView(1, 0, RUNTIME_CLASS(CPaneCView), CSize(320, 120), pContext)) | |||
{ | { | ||
TRACE0("Failed to create pane C\n"); | |||
return FALSE; | return FALSE; | ||
} | } | ||
return TRUE; | return TRUE; | ||
} | } | ||
</ | </syntaxhighlight> | ||
<br> | |||
サブスプリッタの親ウィンドウには、メインスプリッタのアドレス <u>&m_SplitMain</u> を指定する。<br> | |||
第5引数の子ウィンドウIDには、<u>m_SplitMain.IdFromRowCol(0, 1)</u> を使用して、メインスプリッタの右ペインのIDを取得する。<br> | |||
<br> | <br> | ||
この指定が無いと、サブスプリッタが正しい領域に配置されない。<br> | |||
<br><br> | <br><br> | ||
== | == 動的分割ウィンドウ == | ||
動的分割ウィンドウは、<code>Create()</code> を使用して作成する。<br> | |||
<br> | <br> | ||
初期状態では1つのペインが表示され、ユーザがスプリッタボックスをドラッグするか、[ウィンドウ] - [分割]メニューを選択することにより、新しいペインが作成される。<br> | |||
MFCアプリケーションウィザードで[分割ウィンドウ]オプションを有効にすると、雛形が自動生成される。<br> | |||
<br> | <br> | ||
<u>CMainFrame.h</u> にメンバ変数を追加する。<br> | |||
<br> | <br> | ||
<syntaxhighlight lang="c++"> | |||
< | // CMainFrame.h | ||
class CMainFrame : public | class CMainFrame : public CFrameWndEx | ||
{ | { | ||
// ... | // ... | ||
protected: | |||
CSplitterWnd m_wndSplitter; | |||
// ... | // ... | ||
} | }; | ||
</ | </syntaxhighlight> | ||
<br> | <br> | ||
<u>CMainFrame.cpp</u> の <code>OnCreateClient()</code> を以下に示すように定義する。<br> | |||
<br> | <br> | ||
<syntaxhighlight lang="c++"> | |||
< | // CMainFrame.cpp | ||
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT /*lpcs*/, CCreateContext* pContext) | |||
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) | |||
{ | { | ||
return m_wndSplitter.Create( | |||
this, | |||
this, | 2, 2, // 最大行数、最大列数 | ||
CSize(10, 10), // 最小ペインサイズ | |||
CSize( | |||
pContext); | pContext); | ||
} | } | ||
</ | </syntaxhighlight> | ||
<br> | |||
動的スプリッタでは、<code>CreateView()</code> を明示的に呼び出す必要はない。<br> | |||
フレームワークが必要に応じてビューを動的に生成するからである。<br> | |||
<br> | |||
ただし、動的スプリッタの全てのペインは、同じ <code>CView</code> 派生クラスを使用する。<br> | |||
<br><br> | <br><br> | ||
== | == ペイン間のアクセス == | ||
分割ウィンドウ内のあるペインから、別のペインの View オブジェクトにアクセスしたい場合がある。<br> | |||
例えば、左ペインで選択した項目に応じて、右ペインの内容を更新する場合である。<br> | |||
<br> | <br> | ||
各ペインは <code>CSplitterWnd::GetPane</code> メンバ関数で取得できる。<br> | |||
SDI アプリケーションの場合、<code>AfxGetMainWnd</code> を使ってメインフレームを取得し、フレームが保持するスプリッタを経由してアクセスする。<br> | |||
<br> | <br> | ||
以下は、Pane A の <code>OnDraw</code> 内から Pane B および Pane C にアクセスする例である。<br> | |||
< | ネストした3分割の例を前提としている。<br> | ||
CPaneAView.h | <syntaxhighlight lang="c++"> | ||
// CPaneAView.h | |||
#include "CPaneBView.h" | |||
#include "CPaneCView.h" | |||
#include "MainFrm.h" | |||
class CPaneAView : public CFormView | class CPaneAView : public CFormView | ||
{ | { | ||
// ... | // ... | ||
public: | |||
CPaneBView* pPaneB; | |||
CPaneCView* pPaneC; | |||
// ... | |||
}; | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="c++"> | |||
// CPaneAView.cpp | |||
#include "MainFrm.h" | |||
void CPaneAView::OnDraw(CDC* pDC) | |||
{ | |||
// ... | |||
CMainFrame* pFrame = static_cast<CMainFrame*>(AfxGetMainWnd()); | |||
pPaneB = static_cast<CPaneBView*>(pFrame->m_SplitSub.GetPane(0, 0)); | |||
pPaneC = static_cast<CPaneCView*>(pFrame->m_SplitSub.GetPane(1, 0)); | |||
if (pPaneB != nullptr) | |||
{ | |||
pPaneB->Invalidate(); | |||
} | |||
// ... | // ... | ||
} | } | ||
</ | </syntaxhighlight> | ||
< | <br> | ||
<code>GetPane</code> の戻り値は <code>CWnd*</code> なので、使用する際は適切な View クラスにキャストする。<br> | |||
MDI アプリケーションの場合は、<code>AfxGetMainWnd</code> ではなく子フレームへのポインタを取得する必要がある。<br> | |||
<br><br> | |||
== スプリッターバーの固定化 == | |||
ユーザがスプリッターバーをドラッグしてペインサイズを変更できないようにする方法を説明する。<br> | |||
最も確実な方法は、<code>CSplitterWnd</code> の派生クラスを作成し、<code>HitTest</code> メンバ関数をオーバーライドすることである。<br> | |||
<code>HitTest</code> は、指定された座標がスプリッタのどの部分に該当するかを示す整数値を返す。<br> | |||
戻り値が 0 (<code>noHit</code>) の場合、フレームワークはその位置での操作を無視する。<br> | |||
そのため、特定の方向のスプリッターバーに対して 0 を返すことで、その方向の移動を禁止できる。<br> | |||
<br> | |||
まず、<code>CSplitterWnd</code> の派生クラスを作成する。<br> | |||
<syntaxhighlight lang="c++"> | |||
// CMySplitterWnd.h | |||
#pragma once | |||
#include <afxext.h> | |||
class CMySplitterWnd : public CSplitterWnd | |||
{ | |||
DECLARE_DYNAMIC(CMySplitterWnd) | |||
public: | |||
CMySplitterWnd(); | |||
virtual ~CMySplitterWnd(); | |||
virtual int HitTest(CPoint pt) const; | |||
}; | |||
</syntaxhighlight> | |||
<syntaxhighlight lang="c++"> | |||
// CMySplitterWnd.cpp | |||
#include "pch.h" | |||
#include "CMySplitterWnd.h" | |||
IMPLEMENT_DYNAMIC(CMySplitterWnd, CSplitterWnd) | |||
CMySplitterWnd::CMySplitterWnd() {} | |||
CMySplitterWnd::~CMySplitterWnd() {} | |||
int CMySplitterWnd::HitTest(CPoint pt) const | |||
{ | |||
int ht = CSplitterWnd::HitTest(pt); | |||
// 縦方向のスプリッターバーと交差点を無効化 | |||
if (ht >= hSplitterBar1 && ht <= hSplitterBar15) | |||
{ | |||
ht = noHit; | |||
} | |||
if (ht >= splitterIntersection1 && ht <= splitterIntersection225) | |||
{ | |||
ht = noHit; | |||
} | |||
return ht; | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
上記の例では、縦方向のスプリッターバーと交差点での操作を無効化している。<br> | |||
横方向のスプリッターバーだけ有効にしたい場合は、縦方向と交差点のみを 0 にすればよい。<br> | |||
<code>noHit</code>、<code>hSplitterBar1</code>、<code>splitterIntersection1</code> 等の値は <code>CSplitterWnd</code> の実装内で定義されており、必要に応じて独自に定義する。<br> | |||
<br><br> | |||
== ウィンドウリサイズ対応 == | |||
フレームウィンドウがリサイズされたときに、ペインのサイズを一定の比率で保ちたい場合がある。<br> | |||
<br> | |||
その場合、<code>CFrameWnd::OnSize()</code> をオーバーライドして、<code>SetRowInfo()</code> / <code>SetColumnInfo()</code> / <code>RecalcLayout()</code> を呼び出す。<br> | |||
<br> | |||
以下の例では、左右2分割のスプリッタで、右半分を2等分し、左半分も同じ幅に調整している。<br> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// CMainFrame.cpp | |||
void CMainFrame::OnSize(UINT nType, int cx, int cy) | |||
{ | |||
CFrameWndEx::OnSize(nType, cx, cy); | |||
if (m_bSplitterCreated && nType != SIZE_MINIMIZED) | |||
{ | |||
int nHalf = cx / 2; | |||
m_SplitMain.SetColumnInfo(0, nHalf, 50); | |||
m_SplitMain.SetColumnInfo(1, nHalf, 50); | |||
m_SplitMain.RecalcLayout(); | |||
} | |||
} | |||
</syntaxhighlight> | |||
<br> | |||
上記の例では、<u>m_bSplitterCreated</u> フラグでスプリッタの作成完了を確認している。<br> | |||
このフラグは <code>OnCreateClient()</code> でスプリッタ作成成功後に <u>TRUE</u> に設定する。<br> | |||
<br> | |||
<code>OnSize()</code> はフレーム作成時にスプリッタがまだ存在しない段階でも呼ばれる可能性があるため、このチェックは必須である。<br> | |||
<br> | |||
ネストしたスプリッタの場合は、メインスプリッタとサブスプリッタの両方で <code>RecalcLayout()</code> を呼び出す必要がある。<br> | |||
<br> | |||
<syntaxhighlight lang="c++"> | |||
// ネストしたスプリッタの例 | |||
void CMainFrame::OnSize(UINT nType, int cx, int cy) | |||
{ | { | ||
CFrameWndEx::OnSize(nType, cx, cy); | |||
if (m_bSplitterCreated && nType != SIZE_MINIMIZED) | |||
{ | |||
int nLeft = cx / 3; | |||
int nRight = cx - nLeft; | |||
m_SplitMain.SetColumnInfo(0, nLeft, 50); | |||
m_SplitMain.SetColumnInfo(1, nRight, 50); | |||
m_SplitMain.RecalcLayout(); | |||
int nRightHalf = nRight / 2; | |||
m_SplitSub.SetRowInfo(0, nRightHalf, 30); | |||
m_SplitSub.SetRowInfo(1, nRightHalf, 30); | |||
m_SplitSub.RecalcLayout(); | |||
} | |||
} | } | ||
</ | </syntaxhighlight> | ||
<br><br> | |||
== トラブルシューティング == | |||
==== 問題1: ウィンドウが正しく表示されない ==== | |||
* 原因 | |||
*: 静的スプリッタでは、<code>OnCreateClient()</code> が戻る前に全てのペインを <code>CreateView</code> で作成していない。 | |||
* 解決方法 | |||
*: <code>CreateStatic()</code> 呼び出し後、全ての行 / 列に対して <code>CreateView()</code> を呼び出す。 | |||
<br> | |||
==== 問題2: 分割線がドラッグできない ==== | |||
* 原因 | |||
*: <code>CreateStatic()</code> の第4引数で <code>WS_VISIBLE</code> を指定していない、または、ペインの最小幅 / 高さが大きすぎる。 | |||
* 解決方法 | |||
*: スタイルに <code>WS_CHILD | WS_VISIBLE</code> を含める。 | |||
*: 必要に応じて <code>SetRowInfo()</code> / <code>SetColumnInfo()</code> の最小幅を小さくする。 | |||
<br> | |||
==== 問題3: サブスプリッタが正しい領域に表示されない ==== | |||
* 原因 | |||
*: ネストしたスプリッタの子ウィンドウIDが正しく指定されていない。 | |||
* 解決方法 | |||
*: 親スプリッタの <code>IdFromRowCol</code> を使用して、配置先のペインIDを取得する。 | |||
<br> | |||
==== 問題4: リサイズ時にアプリケーションがクラッシュする ==== | |||
* 原因 | |||
*: <code>OnSize()</code> 内で <code>RecalcLayout()</code> を呼び出したが、スプリッタがまだ作成されていない。 | |||
* 解決方法 | |||
*: スプリッタ作成完了を示すフラグ変数を用意して、<code>OnSize()</code> 内でチェックする。 | |||
<br> | |||
==== 問題5: 別のペインにアクセスできない ==== | |||
* 原因 | |||
*: <code>GetPane()</code> の戻り値を適切なViewクラスにキャストしていない、または、MDIで子フレームを取得していない。 | |||
* 解決方法 | |||
*: <code>static_cast</code> で正しい型にキャストする。 | |||
*: MDIの場合は、<code>CMDIChildWnd</code> 経由で子フレームを取得する。 | |||
<br><br> | <br><br> | ||
== 主要メソッド一覧 == | |||
下表に、<code>CSplitterWnd</code> クラスでよく使用されるメソッドを示す。<br> | |||
<br> | |||
<center> | |||
{| class="wikitable" | |||
|+ CSplitterWndクラスの主要メソッド | |||
! メソッド名 !! 説明 | |||
|- | |||
| Create || 動的分割ウィンドウを作成する。 | |||
|- | |||
| CreateStatic || 静的分割ウィンドウを作成する。 | |||
|- | |||
| CreateView || 静的スプリッタの指定位置にビューを作成する。 | |||
|- | |||
| SetRowInfo || 行の理想サイズと最小幅を設定する。 | |||
|- | |||
| SetColumnInfo || 列の理想サイズと最小幅を設定する。 | |||
|- | |||
| GetRowInfo || 行の現在サイズと最小幅を取得する。 | |||
|- | |||
| GetColumnInfo || 列の現在サイズと最小幅を取得する。 | |||
|- | |||
| GetPane || 指定行 / 列のペイン (CWnd*) を取得する。 | |||
|- | |||
| GetRowCount || 現在の行数を取得する。 | |||
|- | |||
| GetColumnCount || 現在の列数を取得する。 | |||
|- | |||
| IdFromRowCol || 指定行 / 列の子ウィンドウIDを取得する。 | |||
|- | |||
| RecalcLayout || 行 / 列サイズ変更後にレイアウトを再計算する。 | |||
|- | |||
| GetActivePane || アクティブなペインを取得する。 | |||
|- | |||
| SetActivePane || 指定ペインをアクティブにする。 | |||
|} | |||
</center> | |||
<br><br> | |||
== 関連情報 == | |||
* [https://learn.microsoft.com/ja-jp/cpp/mfc/reference/csplitterwnd-class Microsoft Learn - CSplitterWndクラス] | |||
* [https://learn.microsoft.com/en-us/cpp/mfc/tn029-splitter-windows Microsoft Learn - TN029: Splitter Windows] | |||
* [https://github.com/microsoft/VCSamples/tree/master/VC2010Samples/MFC/general/viewex Microsoft VCSamples - VIEWEX] | |||
<br><br> | |||
{{#seo: | |||
|title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki | |||
|keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,電気回路,電子回路,基板,プリント基板 | |||
|description={{PAGENAME}} - MFCアプリケーションの分割ウィンドウ (CSplitterWnd) に関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux | |||
|image=/resources/assets/MochiuLogo_Single_Blue.png | |||
}} | |||
__FORCETOC__ | __FORCETOC__ | ||
[[カテゴリ:MFC]] | [[カテゴリ:MFC]] | ||
2026年6月16日 (火) 17:23時点における最新版
概要
分割ウィンドウとは、ひとつのフレームウィンドウのクライアント領域を複数のペインに分割して表示するウィンドウ構成である。
Microsoft Foundation Classes (MFC) では、CSplitterWnd クラスを使用してこの機能を実現できる。
各ペインは通常、異なる CView 派生クラス、または 同じ CView 派生クラスのインスタンスを割り当てる。
MFCの分割ウィンドウには、静的スプリッタと動的スプリッタの2種類が存在する。
静的スプリッタは CreateStatic() を使用して、作成時に決めた行数と列数のペインを全て生成する。
ユーザはペインのサイズを変更できるが、ペインの数や順序は変更できない。
<br.
動的スプリッタは Create() を使用して、初期状態では1つのペインから始まる。
ユーザがスプリッタボックスを操作すると、フレームワークが新しいペインを動的に作成・破棄する。
静的スプリッタは異なるビュークラスを組み合わせて、複数の情報を同時に表示する用途に適している。
動的スプリッタは、同じドキュメントを複数の視点やスクロール位置で確認する用途に適している。
CSplitterWnd クラスは CFrameWnd または CMDIChildWnd の派生クラスに埋め込んで使用する。
実装の中核は、フレームクラスの OnCreateClient() をオーバーライドして、その中で CreateStatic() または Create() を呼び出すことである。
その後、静的スプリッタの場合は CreateView() を使用して、各ペインにビューを割り当てる必要がある。
分割ウィンドウは、ドキュメント/ビューアーキテクチャと組み合わせることで、複雑なユーザインターフェースを比較的少ないコードで構築できる。
ただし、MFCは引き続きサポートされているが、新機能の追加やドキュメントの更新は行われていないことに注意すること。
前提条件
分割ウィンドウを理解するためには、以下に示す知識と環境が必要である。
- C++によるデスクトップ開発ワークロードを有効にする。
- ドキュメント / ビューアーキテクチャの基礎知識
CWinApp、CDocument、CView、CFrameWndの関係を理解していること。
- C++とWindows APIの知識
- ポインタ、参照、メッセージハンドラ、
CRuntimeClassクラス等を理解していること。
- ポインタ、参照、メッセージハンドラ、
DECLARE_DYNCREATE/IMPLEMENT_DYNCREATEマクロの理解- 分割ウィンドウの各ペインに割り当てるViewクラスは、動的生成が可能である必要がある。
分割ウィンドウの種類
MFCで提供される分割ウィンドウには、静的スプリッタと動的スプリッタの2種類がある。
用途に応じて適切な方を選択すること。
| 項目 | 静的スプリッタ (CreateStatic) |
動的スプリッタ (Create)
|
|---|---|---|
| ペイン数 | 固定 | ユーザ操作で増減 |
| 最大分割数 | 16行 x 16列 | 2行 x 2列 |
| ペインのクラス | 異なる CView 派生クラスを割り当て可能 |
基本的に同じ CView 派生クラス
|
| スクロールバー | 共有スクロールバーを WS_HSCROLL / WS_VSCROLL で追加可能 |
動的に生成されるペインと連動 |
| 使用例 | Windowsエクスプローラー Visual Studioのグラフィックスエディタ |
Microsoft Excel Microsoft Word |
| 主な用途 | 異なる情報を同じウィンドウ内に同時表示 | 同じドキュメントを複数視点で確認 |
静的スプリッタを使用する場合、全てのペインを OnCreateClient() 内で明示的に作成する必要がある。
動的スプリッタを使用する場合、フレームワークがペインの作成と破棄を自動的に行う。
基本的な実装手順
分割ウィンドウを実装する時の流れを以下に示す。
- フレームクラスに
CSplitterWndクラスのメンバ変数を追加する。- SDIアプリケーションの場合は、
CMainFrameクラス、MDIアプリケーションの場合は、CChildFrameクラスに追加する。
- SDIアプリケーションの場合は、
- フレームクラスの
OnCreateClient()をオーバーライドする。- クラスビューから
OnCreateClient()を追加する、または、ヘッダファイルで手動で宣言する。
- クラスビューから
OnCreateClient()内でCreateStatic()またはCreate()を呼び出す。- 静的スプリッタの場合は
CreateStatic()、動的スプリッタの場合はCreate()を使用する。
- 静的スプリッタの場合は
- 静的スプリッタの場合、全てのペインに
CreateView()でビューを割り当てる。- 各ペインに対応する
CView派生クラスをRUNTIME_CLASSマクロで指定する。
- 各ペインに対応する
- 必要に応じて、
SetRowInfo()/SetColumnInfo()でサイズを調整する。- リサイズ時には、
OnSize()ハンドラで再調整することもある。
- リサイズ時には、
下図は、ドキュメント / ビューアーキテクチャにおける分割ウィンドウの位置づけを示している。

静的分割ウィンドウ
静的分割ウィンドウは、CreateStatic() を使用して作成する。
ユーザはペインの境界線をドラッグしてサイズを変更できるが、ペインの数は増減しない。
左右2分割の作成
1行2列の分割ウィンドウを作成する例を示す。
左側のペイン (Pane A) にはプロジェクト作成時に生成されたViewクラスを使用し、右側のペイン (Pane B) には新規に作成したViewクラスを使用する。
左側がPaneA (CPaneAView)、右側がPaneB (CPaneBView) となっている。


まず、右ペイン用のViewクラスを追加する。
ソリューションエクスプローラーでプロジェクトを右クリックして、[追加] - [クラス] を選択する。
- クラスの種類
- MFCクラス
- クラス名
- CPaneBView
- 基本クラス
- CFormView または CView
次に、CMainFrame.h に CSplitterWnd のメンバ変数を追加する。
MDIアプリケーションの場合は CChildFrame.h に同様の変更を行う。
// CMainFrame.h
class CMainFrame : public CFrameWndEx
{
// ...
public:
CSplitterWnd m_SplitMain; // 分割ウィンドウ
// ...
};
次に、CMainFrame.cpp の OnCreateClient() を以下に示すように実装する。
// CMainFrame.cpp
#include "CPaneDoc.h"
#include "CPaneAView.h"
#include "CPaneBView.h"
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
// 1行2列の静的スプリッタを作成
if (!m_SplitMain.CreateStatic(this, 1, 2))
{
TRACE0("Failed to create splitter window\n");
return FALSE;
}
// ペインA (row=0, col=0)
if (!m_SplitMain.CreateView(0, 0, RUNTIME_CLASS(CPaneAView), CSize(200, 0), pContext))
{
TRACE0("Failed to create pane A\n");
return FALSE;
}
// ペインB (row=0, col=1)
if (!m_SplitMain.CreateView(0, 1, RUNTIME_CLASS(CPaneBView), CSize(0, 0), pContext))
{
TRACE0("Failed to create pane B\n");
return FALSE;
}
return TRUE;
}
CreateStatic() の第2引数は行数、第3引数は列数である。
CreateView() の第1引数と第2引数は、それぞれ行番号と列番号を0始まりで指定する。
第3引数には、ペインを管理する CView 派生クラスの CRuntimeClass クラスを RUNTIME_CLASS マクロで指定する。
第4引数の CSize クラスは、理想サイズを示す。
第5引数の pContext は、親フレームから渡された作成コンテキストをそのまま渡す。
ネストした3分割の作成
CreateStatic() は、簡単な n行 x m列 のグリッドしか直接作成できない。
例えば、左側に1つ、右側に上下2つという変則的なレイアウトを作成する場合は、2つの CSplitterWnd クラスをネストする。
具体的には、メインフレームを左右に分割した後、右側のペインをさらに上下に分割する。

まず、Pane A、Pane B、Pane C向けのViewクラスをそれぞれ作成する。
CMainFrame.h には、メインスプリッタとサブスプリッタの2つのメンバ変数を追加する。
// CMainFrame.h
class CMainFrame : public CFrameWndEx
{
// ...
public:
CSplitterWnd m_SplitMain; // メインフレームを左右分割
CSplitterWnd m_SplitSub; // 右ペインを上下分割
// ...
};
次に、CMainFrame.cpp の OnCreateClient() を以下に示すように定義する。
// CMainFrame.cpp
#include "CPaneDoc.h"
#include "CPaneAView.h"
#include "CPaneBView.h"
#include "CPaneCView.h"
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
// メインスプリッタを左右2分割
if (!m_SplitMain.CreateStatic(this, 1, 2))
{
TRACE0("Failed to create main splitter\n");
return FALSE;
}
// サブスプリッタを右ペインに上下2分割
if (!m_SplitSub.CreateStatic(&m_SplitMain, 2, 1, WS_CHILD | WS_VISIBLE | WS_BORDER, m_SplitMain.IdFromRowCol(0, 1)))
{
TRACE0("Failed to create sub splitter\n");
return FALSE;
}
// ペインA (左側)
if (!m_SplitMain.CreateView(0, 0, RUNTIME_CLASS(CPaneAView), CSize(320, 240), pContext))
{
TRACE0("Failed to create pane A\n");
return FALSE;
}
// ペインB (右側上部)
if (!m_SplitSub.CreateView(0, 0, RUNTIME_CLASS(CPaneBView), CSize(320, 120), pContext))
{
TRACE0("Failed to create pane B\n");
return FALSE;
}
// ペインC (右側下部)
if (!m_SplitSub.CreateView(1, 0, RUNTIME_CLASS(CPaneCView), CSize(320, 120), pContext))
{
TRACE0("Failed to create pane C\n");
return FALSE;
}
return TRUE;
}
サブスプリッタの親ウィンドウには、メインスプリッタのアドレス &m_SplitMain を指定する。
第5引数の子ウィンドウIDには、m_SplitMain.IdFromRowCol(0, 1) を使用して、メインスプリッタの右ペインのIDを取得する。
この指定が無いと、サブスプリッタが正しい領域に配置されない。
動的分割ウィンドウ
動的分割ウィンドウは、Create() を使用して作成する。
初期状態では1つのペインが表示され、ユーザがスプリッタボックスをドラッグするか、[ウィンドウ] - [分割]メニューを選択することにより、新しいペインが作成される。
MFCアプリケーションウィザードで[分割ウィンドウ]オプションを有効にすると、雛形が自動生成される。
CMainFrame.h にメンバ変数を追加する。
// CMainFrame.h
class CMainFrame : public CFrameWndEx
{
// ...
protected:
CSplitterWnd m_wndSplitter;
// ...
};
CMainFrame.cpp の OnCreateClient() を以下に示すように定義する。
// CMainFrame.cpp
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT /*lpcs*/, CCreateContext* pContext)
{
return m_wndSplitter.Create(
this,
2, 2, // 最大行数、最大列数
CSize(10, 10), // 最小ペインサイズ
pContext);
}
動的スプリッタでは、CreateView() を明示的に呼び出す必要はない。
フレームワークが必要に応じてビューを動的に生成するからである。
ただし、動的スプリッタの全てのペインは、同じ CView 派生クラスを使用する。
ペイン間のアクセス
分割ウィンドウ内のあるペインから、別のペインの View オブジェクトにアクセスしたい場合がある。
例えば、左ペインで選択した項目に応じて、右ペインの内容を更新する場合である。
各ペインは CSplitterWnd::GetPane メンバ関数で取得できる。
SDI アプリケーションの場合、AfxGetMainWnd を使ってメインフレームを取得し、フレームが保持するスプリッタを経由してアクセスする。
以下は、Pane A の OnDraw 内から Pane B および Pane C にアクセスする例である。
ネストした3分割の例を前提としている。
// CPaneAView.h
#include "CPaneBView.h"
#include "CPaneCView.h"
#include "MainFrm.h"
class CPaneAView : public CFormView
{
// ...
public:
CPaneBView* pPaneB;
CPaneCView* pPaneC;
// ...
};
// CPaneAView.cpp
#include "MainFrm.h"
void CPaneAView::OnDraw(CDC* pDC)
{
// ...
CMainFrame* pFrame = static_cast<CMainFrame*>(AfxGetMainWnd());
pPaneB = static_cast<CPaneBView*>(pFrame->m_SplitSub.GetPane(0, 0));
pPaneC = static_cast<CPaneCView*>(pFrame->m_SplitSub.GetPane(1, 0));
if (pPaneB != nullptr)
{
pPaneB->Invalidate();
}
// ...
}
GetPane の戻り値は CWnd* なので、使用する際は適切な View クラスにキャストする。
MDI アプリケーションの場合は、AfxGetMainWnd ではなく子フレームへのポインタを取得する必要がある。
スプリッターバーの固定化
ユーザがスプリッターバーをドラッグしてペインサイズを変更できないようにする方法を説明する。
最も確実な方法は、CSplitterWnd の派生クラスを作成し、HitTest メンバ関数をオーバーライドすることである。
HitTest は、指定された座標がスプリッタのどの部分に該当するかを示す整数値を返す。
戻り値が 0 (noHit) の場合、フレームワークはその位置での操作を無視する。
そのため、特定の方向のスプリッターバーに対して 0 を返すことで、その方向の移動を禁止できる。
まず、CSplitterWnd の派生クラスを作成する。
// CMySplitterWnd.h
#pragma once
#include <afxext.h>
class CMySplitterWnd : public CSplitterWnd
{
DECLARE_DYNAMIC(CMySplitterWnd)
public:
CMySplitterWnd();
virtual ~CMySplitterWnd();
virtual int HitTest(CPoint pt) const;
};
// CMySplitterWnd.cpp
#include "pch.h"
#include "CMySplitterWnd.h"
IMPLEMENT_DYNAMIC(CMySplitterWnd, CSplitterWnd)
CMySplitterWnd::CMySplitterWnd() {}
CMySplitterWnd::~CMySplitterWnd() {}
int CMySplitterWnd::HitTest(CPoint pt) const
{
int ht = CSplitterWnd::HitTest(pt);
// 縦方向のスプリッターバーと交差点を無効化
if (ht >= hSplitterBar1 && ht <= hSplitterBar15)
{
ht = noHit;
}
if (ht >= splitterIntersection1 && ht <= splitterIntersection225)
{
ht = noHit;
}
return ht;
}
上記の例では、縦方向のスプリッターバーと交差点での操作を無効化している。
横方向のスプリッターバーだけ有効にしたい場合は、縦方向と交差点のみを 0 にすればよい。
noHit、hSplitterBar1、splitterIntersection1 等の値は CSplitterWnd の実装内で定義されており、必要に応じて独自に定義する。
ウィンドウリサイズ対応
フレームウィンドウがリサイズされたときに、ペインのサイズを一定の比率で保ちたい場合がある。
その場合、CFrameWnd::OnSize() をオーバーライドして、SetRowInfo() / SetColumnInfo() / RecalcLayout() を呼び出す。
以下の例では、左右2分割のスプリッタで、右半分を2等分し、左半分も同じ幅に調整している。
// CMainFrame.cpp
void CMainFrame::OnSize(UINT nType, int cx, int cy)
{
CFrameWndEx::OnSize(nType, cx, cy);
if (m_bSplitterCreated && nType != SIZE_MINIMIZED)
{
int nHalf = cx / 2;
m_SplitMain.SetColumnInfo(0, nHalf, 50);
m_SplitMain.SetColumnInfo(1, nHalf, 50);
m_SplitMain.RecalcLayout();
}
}
上記の例では、m_bSplitterCreated フラグでスプリッタの作成完了を確認している。
このフラグは OnCreateClient() でスプリッタ作成成功後に TRUE に設定する。
OnSize() はフレーム作成時にスプリッタがまだ存在しない段階でも呼ばれる可能性があるため、このチェックは必須である。
ネストしたスプリッタの場合は、メインスプリッタとサブスプリッタの両方で RecalcLayout() を呼び出す必要がある。
// ネストしたスプリッタの例
void CMainFrame::OnSize(UINT nType, int cx, int cy)
{
CFrameWndEx::OnSize(nType, cx, cy);
if (m_bSplitterCreated && nType != SIZE_MINIMIZED)
{
int nLeft = cx / 3;
int nRight = cx - nLeft;
m_SplitMain.SetColumnInfo(0, nLeft, 50);
m_SplitMain.SetColumnInfo(1, nRight, 50);
m_SplitMain.RecalcLayout();
int nRightHalf = nRight / 2;
m_SplitSub.SetRowInfo(0, nRightHalf, 30);
m_SplitSub.SetRowInfo(1, nRightHalf, 30);
m_SplitSub.RecalcLayout();
}
}
トラブルシューティング
問題1: ウィンドウが正しく表示されない
- 原因
- 静的スプリッタでは、
OnCreateClient()が戻る前に全てのペインをCreateViewで作成していない。
- 静的スプリッタでは、
- 解決方法
CreateStatic()呼び出し後、全ての行 / 列に対してCreateView()を呼び出す。
問題2: 分割線がドラッグできない
- 原因
CreateStatic()の第4引数でWS_VISIBLEを指定していない、または、ペインの最小幅 / 高さが大きすぎる。
- 解決方法
- スタイルに
WS_CHILD | WS_VISIBLEを含める。 - 必要に応じて
SetRowInfo()/SetColumnInfo()の最小幅を小さくする。
- スタイルに
問題3: サブスプリッタが正しい領域に表示されない
- 原因
- ネストしたスプリッタの子ウィンドウIDが正しく指定されていない。
- 解決方法
- 親スプリッタの
IdFromRowColを使用して、配置先のペインIDを取得する。
- 親スプリッタの
問題4: リサイズ時にアプリケーションがクラッシュする
- 原因
OnSize()内でRecalcLayout()を呼び出したが、スプリッタがまだ作成されていない。
- 解決方法
- スプリッタ作成完了を示すフラグ変数を用意して、
OnSize()内でチェックする。
- スプリッタ作成完了を示すフラグ変数を用意して、
問題5: 別のペインにアクセスできない
- 原因
GetPane()の戻り値を適切なViewクラスにキャストしていない、または、MDIで子フレームを取得していない。
- 解決方法
static_castで正しい型にキャストする。- MDIの場合は、
CMDIChildWnd経由で子フレームを取得する。
主要メソッド一覧
下表に、CSplitterWnd クラスでよく使用されるメソッドを示す。
| メソッド名 | 説明 |
|---|---|
| Create | 動的分割ウィンドウを作成する。 |
| CreateStatic | 静的分割ウィンドウを作成する。 |
| CreateView | 静的スプリッタの指定位置にビューを作成する。 |
| SetRowInfo | 行の理想サイズと最小幅を設定する。 |
| SetColumnInfo | 列の理想サイズと最小幅を設定する。 |
| GetRowInfo | 行の現在サイズと最小幅を取得する。 |
| GetColumnInfo | 列の現在サイズと最小幅を取得する。 |
| GetPane | 指定行 / 列のペイン (CWnd*) を取得する。 |
| GetRowCount | 現在の行数を取得する。 |
| GetColumnCount | 現在の列数を取得する。 |
| IdFromRowCol | 指定行 / 列の子ウィンドウIDを取得する。 |
| RecalcLayout | 行 / 列サイズ変更後にレイアウトを再計算する。 |
| GetActivePane | アクティブなペインを取得する。 |
| SetActivePane | 指定ペインをアクティブにする。 |
関連情報
- Microsoft Learn - CSplitterWndクラス
- Microsoft Learn - TN029: Splitter Windows
- Microsoft VCSamples - VIEWEX