2 Synchornized TreeView controls

There are business scenarios that need to keep two separate TreeView controls synced, one of them can be content comparison. I was surprised that creating a form with two TreeView controls with synchronised Expand/Collapse and Scroll actions cannot be done without P/Invoke. Here a complete solution. This article does not cover how to fill nodes collection for content comparison, does only cover how to reflect changes on one TreeView scroll and node position onto another.

Synced TreeView expand:

Synced TreeView expand 1 Synced TreeView expand 2

Synced TreeView scroll:

Synced TreeView scroll 1 Synced TreeView scroll 2

First of all we need to get scroll position info from the TreeView control, to do that we need to override WndProc method:

        protected override void WndProc(ref Message m) {
            base.WndProc(ref m);
            switch (m.Msg) {
                case WM_VSCROLL:
                case WM_MOUSEWHEEL:
                    OnScroll(ScrollBar.Vertical);
                    break;
                case WM_HSCROLL:
                    OnScroll(ScrollBar.Horizontal);
                    break;
            }
        }

It’s the easy part 😉 . OnScroll is an event that we’ll implement it later on. Now we need to use User32.Dll imported methods:

        [DllImport("user32.dll")]
        private static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO scrollInfo);

        [DllImport("user32.dll")]
        private static extern int SetScrollInfo(IntPtr hwnd, int fnBarm, ref SCROLLINFO scrollInfo, bool fRedraw);

I want to be able to get scroll position and set it. I will do that with a couple of handy methods built on top of imported ones:

        private SCROLLINFO GetScrollInfo(ScrollBar scroll, ScrollInfoMask mask) {
            SCROLLINFO si = SCROLLINFO.Create();
            si.fMask = (int) mask;
            GetScrollInfo(Handle, (int) scroll, ref si);
            return si;
        }

        public int GetScrollPosition(ScrollBar type) {
            SCROLLINFO si = GetScrollInfo(type, ScrollInfoMask.SIF_POS);
            return si.nPos;
        }

        public void SetScrollPosition(ScrollBar type, int position) {
            SCROLLINFO si = SCROLLINFO.Create();

            // We are setting only the position part
            si.fMask = (int) ScrollInfoMask.SIF_POS;
            si.nPos = position;

            SetScrollInfo(Handle, (int) type, ref si, true);

            // Yick!
            // This forces treeView to redraw it'self.
            // Invalidate and firedns does not do that, and simple scroll bar position set
            // does ONLY scroll the scroll bar leaving treeView contents untouched.
            //
            // Please let me know if you know better, less expensive way to scrooll contents.
            this.ResizeRedraw = true;
            this.Height += 1;
            this.Height -= 1;
        }

Please forgive me for an ugly workaround. TreeView does not completely reflect changes made to it’s scroll bar.

That’s all major parts of our TreeView control extended with scroll bar manipulation, here the complete source code:

/* Copyright 2007 Janusz Skonieczny
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Created by: WooYek on 09:20:43 2007-08-15
 *
 * Last changes made by:
 * $Id: TreeViewExt.cs 4132 2007-08-16 08:46:17Z wooyek $
 */

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WooYek.Windows.Forms {
    public class TreeViewExt : TreeView {
        private const int WM_VSCROLL = 0x0115;
        private const int WM_HSCROLL = 0x0114;
        private const int WM_MOUSEWHEEL = 0x020a;

        [DllImport("user32.dll")]
        private static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO scrollInfo);

        [DllImport("user32.dll")]
        private static extern int SetScrollInfo(IntPtr hwnd, int fnBarm, ref SCROLLINFO scrollInfo, bool fRedraw);

        private SCROLLINFO GetScrollInfo(ScrollBar scroll, ScrollInfoMask mask) {
            SCROLLINFO si = SCROLLINFO.Create();
            si.fMask = (int) mask;
            GetScrollInfo(Handle, (int) scroll, ref si);
            return si;
        }

        public int GetScrollPosition(ScrollBar type) {
            SCROLLINFO si = GetScrollInfo(type, ScrollInfoMask.SIF_POS);
            return si.nPos;
        }

        public void SetScrollPosition(ScrollBar type, int position) {
            SCROLLINFO si = SCROLLINFO.Create();

            // We are setting only the position part
            si.fMask = (int) ScrollInfoMask.SIF_POS;
            si.nPos = position;

            SetScrollInfo(Handle, (int) type, ref si, true);

            // Yick!
            // This forces treeView to redraw it'self.
            // Invalidate and firedns does not do that, and simple scroll bar position set
            // does ONLY scroll the scroll bar leaving treeView contents untouched.
            //
            // Please let me know if you know better, less expensive way to scrooll contents.
            this.ResizeRedraw = true;
            this.Height += 1;
            this.Height -= 1;
        }

        public event ScrollHandler Scroll;

        protected virtual void OnScroll(ScrollBar type) {
            // We don't put all scroll info here, assuming PInvoke calls are expesive.
            // IMHO getting scroll infor later, as needed is more effective and less expensive.
            if (Scroll != null) {
                Scroll(this, new ScrollEventArgs(type));
            }
        }

        public delegate void ScrollHandler(object sender, ScrollEventArgs e);

        public class ScrollEventArgs : EventArgs {
            private ScrollBar type;

            public ScrollEventArgs(ScrollBar type) {
                this.type = type;
            }

            public ScrollBar Type {
                get { return type; }
            }
        }

        protected override void WndProc(ref Message m) {
            base.WndProc(ref m);
            switch (m.Msg) {
                case WM_VSCROLL:
                case WM_MOUSEWHEEL:
                    OnScroll(ScrollBar.Vertical);
                    break;
                case WM_HSCROLL:
                    OnScroll(ScrollBar.Horizontal);
                    break;
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct SCROLLINFO {
            public int cbSize; // sizeof(SCROLLINFO);
            public int fMask;
            public int nMin;
            public int nMax;
            public int nPage;
            public int nPos;
            public int nTrackPos;

            /// <summary>
            /// Sets <see cref="cbSize"/>. No need to use unsafe sizeof call.
            /// </summary>
            /// <returns></returns>
            public static SCROLLINFO Create() {
                SCROLLINFO si = new SCROLLINFO();
                si.cbSize = 28;
                return si;
            }
        }

        public enum ScrollBar : int {
            /// <summary>
            /// SB_HORZ = 0;
            /// </summary>
            Horizontal = 0,
            /// <summary>
            /// SB_VERT = 1;
            /// </summary>
            Vertical = 1
        }

        public enum ScrollInfoMask : int {
            SIF_TRACKPOS = 0x10,
            SIF_RANGE = 0x1,
            SIF_PAGE = 0x2,
            SIF_POS = 0x4,
            SIF_DISABLENOSCROLL = 0x8,
            SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS
        }
    }
}

Now that we have a control that we can use, lest make a simple example application:

/* Copyright 2007 Janusz Skonieczny
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Created by: WooYek on 12:15:43 2007-08-16
 *
 * Last changes made by:
 * $Id: $
 */
using System.Collections;
using System.Windows.Forms;
using WooYek.Windows.Forms;

namespace SyncedTreeView {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();

            FillNodes(treeView1.Nodes, 0, null);
            FillNodes(treeView2.Nodes, 0, null);

            treeView1.Scroll += TreeView1_OnScroll;
            treeView2.Scroll += TreeView2_OnScroll;
            treeView1.AfterExpand += TreeView1_OnAfterExpand;
            treeView2.AfterExpand += TreeView2_OnAfterExpand;
            treeView1.AfterCollapse += TreeView1_OnAfterCollapse;
            treeView2.AfterCollapse += TreeView2_OnAfterCollapse;
        }

        private void TreeView2_OnScroll(object sender, TreeViewExt.ScrollEventArgs e) {
            int position = treeView2.GetScrollPosition(e.Type);
            treeView1.SetScrollPosition(e.Type, position);
        }

        private void TreeView1_OnScroll(object sender, TreeViewExt.ScrollEventArgs e) {
            int position = treeView1.GetScrollPosition(e.Type);
            treeView2.SetScrollPosition(e.Type, position);
        }

        private void TreeView2_OnAfterCollapse(object sender, TreeViewEventArgs e) {
            TreeNode node = e.Node;
            int[] nodeIndicies = GetNodeIndicies(node);
            Collapse(treeView1.Nodes, nodeIndicies);
        }

        private void TreeView1_OnAfterCollapse(object sender, TreeViewEventArgs e) {
            TreeNode node = e.Node;
            int[] nodeIndicies = GetNodeIndicies(node);
            Collapse(treeView2.Nodes, nodeIndicies);
        }

        private void TreeView2_OnAfterExpand(object sender, TreeViewEventArgs e) {
            TreeNode node = e.Node;
            int[] nodeIndicies = GetNodeIndicies(node);
            Expand(treeView1.Nodes, nodeIndicies);
        }

        private void TreeView1_OnAfterExpand(object sender, TreeViewEventArgs e) {
            TreeNode node = e.Node;
            int[] nodeIndicies = GetNodeIndicies(node);
            Expand(treeView2.Nodes, nodeIndicies);
        }

        /// <summary>
        /// Returns an array of node <see cref="TreeNode.Index">iddicies</see>,
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private static int[] GetNodeIndicies(TreeNode node) {
            ArrayList l = new ArrayList();
            while (node != null) {
                l.Add(node.Index);
                node = node.Parent;
            }
            l.Reverse();
            return (int[]) l.ToArray(typeof (int));
        }

        /// <summary>
        /// Does expand all nodes on the given indicies.
        /// </summary>
        /// <param name="nodes"></param>
        /// <param name="indicies"></param>
        public void Expand(TreeNodeCollection nodes, params int[] indicies) {
            foreach (int i in indicies) {
                TreeNode node = nodes[i];
                if (!node.IsExpanded) {
                    node.Expand();
                }
                nodes = node.Nodes;
            }
        }

        /// <summary>
        /// Does collapse ONE node indicated by the last given index.
        /// </summary>
        /// <param name="nodes"></param>
        /// <param name="indicies"></param>
        public void Collapse(TreeNodeCollection nodes, params int[] indicies) {
            //Guard.ArrayNotEmpty(indicies, "indicies");
            TreeNode node = null;
            foreach (int i in indicies) {
                node = nodes[i];
                nodes = node.Nodes;
            }
            if (node.IsExpanded) {
                node.Collapse();
            }
        }

        /// <summary>
        /// A helper method, filling nodes with test data.
        /// </summary>
        /// <param name="nodes"></param>
        /// <param name="level"></param>
        /// <param name="prefix"></param>
        private void FillNodes(TreeNodeCollection nodes, int level, string prefix) {
            if (++level > 3) {
                return;
            }
            for (int i = 1; i < 10; i++) {
                string nodeText = prefix != null ? prefix + "." + i : i.ToString();
                TreeNode node = nodes.Add(nodeText);
                FillNodes(node.Nodes, level, nodeText);
            }
        }
    }
}

I did not use TreeView.FullPath because I do not expect for TreeView nodes to be the same. I just expect exact same number of elements on each level for both controls. I think the rest is self explanatory, but feel free to ask questions:

Here’s a complete project source code.

Leave a Reply

Back to Top