Repository navigation
Description
Description
Description
The image compare splitter always lays out as an equal split. A dragged position is discarded by:
- resizing the window
- maximizing or restoring
- toggling View > Split Vertically
- opening the next image comparison
- restarting WinMerge
Expected: the position is kept.
Use case: Dragging Mode > Vertical Wipe with one pane reduced to a narrow strip (would also be nice if it could be made smaller than 32px).
Opened WinMerge/winimerge#39
Have a WinMerge PR ready and waiting for your approval.
Examples or references (optional)
WinIMerge, at 57ef929:
| Location | Relevance |
|---|---|
ImgMergeWindow.hpp CalcChildImgWindowRect() :1264 |
Computes the equal split. Stateless |
OnSize() :1406 |
Calls it on every WM_SIZE |
MoveSplitter() :1308 |
Applies a drag to the child rects only |
SetHorizontalSplit() :213 |
Re-lays out through the same function |
WinIMergeLib.h |
No splitter property. GetPaneWindowRect() has no setter counterpart |
WinMerge:
| Location | Relevance |
|---|---|
Src/ImgMergeFrm.cpp LoadOptions() |
Runs in OnCreateClient(), before OpenImages() |
Src/ImgMergeFrm.cpp SaveOptions() |
Runs from DestroyWindow() |
Src/OptionsDef.h, Src/OptionsInit.cpp |
Existing Settings/Image... options |
No existing request found in either repository.
Additional context
The value must live in CImgMergeWindow and be read by CalcChildImgWindowRect(). Storing it only on the WinMerge side does not survive, because OnSize() rebuilds the layout from scratch.
Two repositories: a WinIMerge change, then a submodule bump and the options wiring in WinMerge.
The interface is declared in three files. Src/ImgMergeFrm.h includes "WinIMergeLib.h", which resolves to WinMerge's tracked copy at Src/WinIMergeLib.h, not the one in Externals/winimerge. Patching only the submodule copy builds WinIMergeLib and then fails in Merge.vcxproj with C2039: 'SetSplitterPosition': is not a member of 'IImgMergeWindow'.
Design decisions
| Decision | Options |
|---|---|
| Unit | Pixels, or a fraction of the window. Pixels keep a narrow strip narrow when the window grows; a fraction scales it up |
| Which pane the value sizes | The first pane, or whichever pane is smaller. Anchoring the first pane only preserves a narrow strip on the left; a narrow right-hand pane grows on resize. Sizing the smaller pane needs a sign to say which pane it refers to |
| Orientation change | Reset the value in SetHorizontalSplit(), or keep a per-orientation pair of options. A single unsigned axis-agnostic value would apply a width as a height. Note Settings/SplitHorizontally is shared with the text compare and is saved in OnViewSplitVertically(), not SaveOptions() |
| Three-pane compare | Leave on the equal split, or store a position per splitter |
The sketch below implements: pixels, smaller pane with a sign, reset on orientation change, two panes only.
Status
Implemented and built against 57ef929 plus current WinMerge master, on Windows 11 x64 with VS Build Tools 2026. Verified in the standalone WinIMerge app and in WinMergeU.exe: position kept across resize, maximize/restore, reopening a comparison and restart, with the narrow pane on either side; default centred layout unchanged when never dragged; 32px minimum kept; three-pane unaffected.
Implementation sketch
WinIMergeLib.h, and Src/WinIMergeLib.h in the WinMerge repo
Appended, so the vtable order is unchanged:
virtual int GetSplitterPosition() const = 0;
virtual void SetSplitterPosition(int position) = 0;ImgMergeWindow.hpp
Member, initialised to 0:
// Size along the split axis of whichever pane is smaller, in pixels:
// positive for the first pane, negative for the second. 0 = equal split.
int m_splitterPosition;Bounds match MoveSplitter(), so a restored layout equals the dragged one:
int CalcSplitterPosition(int nImages, LONG splitSize) const
{
const int minSize = 32;
const int total = static_cast<int>(splitSize);
const int maxPos = total - minSize;
if (nImages != 2 || m_splitterPosition == 0 || maxPos < minSize)
return 0;
const int pos = (m_splitterPosition > 0)
? m_splitterPosition
: total - 2 * 2 + m_splitterPosition;
return (std::min)((std::max)(pos, minSize), maxPos);
}The clamp is returned, not written back to m_splitterPosition: the window is laid out once during OnCreateClient() before it has its final size. (std::min) / (std::max) are parenthesised as elsewhere in this file, which does not define NOMINMAX.
CalcChildImgWindowRect() needs a separate branch rather than a substituted width, because the existing tail derives the last pane's left edge from width (rc.left = rc.right - width - cx;), which holds only for an equal split:
if (!bHorizontalSplit)
{
const int splitPos = CalcSplitterPosition(nImages, rcParent.right);
if (splitPos > 0)
{
rc.left = 0;
rc.right = splitPos;
childrects.push_back(rc);
rc.left = rc.right + 2 * 2;
rc.right = rcParent.right;
childrects.push_back(rc);
}
else
{
// existing equal-split code, unchanged
}
}Mirrored on rcParent.bottom / rc.top / rc.bottom for the horizontal case.
MoveSplitter(), after the existing clamping, before the closing SetWindowRect() loop:
if (m_nImages == 2 && m_nDraggingSplitter == 0)
{
const int first = m_bHorizontalSplit
? rc[0].bottom - rc[0].top
: rc[0].right - rc[0].left;
const int second = m_bHorizontalSplit
? rc[1].bottom - rc[1].top
: rc[1].right - rc[1].left;
m_splitterPosition = (second < first) ? -second : first;
}OnLButtonUp() resets m_nDraggingSplitter only after MoveSplitter() returns. Sizes rather than rc[0].right, because ImgWindow::GetWindowRect() is relative to the parent's window origin, not its client origin.
Accessors:
int GetSplitterPosition() const override
{
return m_splitterPosition;
}
void SetSplitterPosition(int position) override
{
m_splitterPosition = position;
if (m_hWnd && m_nImages == 2)
{
std::vector<RECT> rects = CalcChildImgWindowRect(m_hWnd, m_nImages, m_bHorizontalSplit);
for (int i = 0; i < m_nImages; ++i)
m_imgWindow[i].SetWindowRect(rects[i]);
}
}Stored before the m_hWnd check, unlike SetHorizontalSplit(), which returns early and drops its argument. LoadOptions() calls this while m_nImages is 0; the value is applied by the OpenImages() / NewImages() call to CalcChildImgWindowRect().
SetHorizontalSplit(), before m_bHorizontalSplit is assigned:
if (m_bHorizontalSplit != horizontalSplit)
m_splitterPosition = 0;WinMerge
// OptionsDef.h
inline const String OPT_CMP_IMG_SPLITTER_POS {_T("Settings/ImageSplitterPosition"s)};// OptionsInit.cpp
pOptions->InitOption(OPT_CMP_IMG_SPLITTER_POS, 0, -65535, 65535);// ImgMergeFrm.cpp, LoadOptions(), after SetHorizontalSplit()
m_pImgMergeWindow->SetSplitterPosition(GetOptionsMgr()->GetInt(OPT_CMP_IMG_SPLITTER_POS));// ImgMergeFrm.cpp, SaveOptions()
GetOptionsMgr()->SaveOption(OPT_CMP_IMG_SPLITTER_POS, m_pImgMergeWindow->GetSplitterPosition());GetPaneWindowRect(0) would return the current clamped rectangle rather than the stored preference, so a getter is needed.
No new UI.
Unrelated: the horizontal branch of CalcChildImgWindowRect() uses GetSystemMetrics(SM_CXVSCROLL) as a height; SM_CYHSCROLL appears to be intended. Both metrics are equal by default, so there is no visible effect. Untouched by the sketch.
Activity
sdottaka commentedon Aug 29, 2026
I was wondering if what you really want might be a single-pane view rather than persistent splitter position.
If we persist the splitter position, I think we would also need a way to restore the default 50:50 split. As I understand it, the current implementation doesn't provide a way to do that. Is my understanding correct?
Ikkoru commentedon Aug 29, 2026
I did indeed want a single pane view :3
However this way introduced less settings / UI options while giving pretty much the same effect, so I went with it.
Currently the 50:50 split is returned on horizontal <-> vertical swap. And you can always add a
Reset splitter positionsomewhere if you want. Or a double-click event on the splitter that resets it.sdottaka commentedon Aug 30, 2026
I’m planning to merge the PR WinMerge/winimerge#39 for now.
My current plan is:
Ikkoru commentedon Aug 30, 2026
I have a PR ready for WinMerge too. Should I post it? You might be able to reuse code from it.
sdottaka commentedon Aug 30, 2026
I’ve merged the PR WinMerge/winimerge#39. Please go ahead and post the WinMerge PR!
sdottaka commentedon Aug 30, 2026
I’ve merged Remember the image compare splitter position #3593.
Please note that I plan to make the splitter position persistent only when Window → Keep Splitter Position is checked.