내용 보기

작성자

관리자 (IP : 172.17.0.1)

날짜

2020-07-10 03:49

제목

[WPF] 특정 컨트롤 스크롤 고정 Panel


Scroll Freeze Panel in WPF


using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
 
namespace Iimaginec.Lib.Controls.Panels
{
public enum ScrollBehaviours
{
Scrollable = 0,
NonScrollable,
AlwaysVisible
}
 
public class BooleanToAlwaysVisibleScrollBehaviour : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var b = (bool)value;
return b ? ScrollBehaviours.AlwaysVisible : ScrollBehaviours.Scrollable;
}
 
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (ScrollBehaviours)value == ScrollBehaviours.AlwaysVisible;
}
}
 
/// <summary>
/// ScrollFreezePanel arranges elements similar to a StackPanel, but has special
/// behaviour when attached to a ScrollViewer. To use this control, it must be
/// nested in a ScrollViewer with CanContentScroll set to true.
///
/// Children of this panel can request special scrolling behaviour via the
/// ScrollBehaviour attached property:
///
/// Scrollable - default behaviour
///
/// NonScrollable - element will remain in place while other children scroll.
///
/// AlwaysVisible - element will scroll within the visible area, but will remain
/// in place once it hits the edge.
///
/// NOTE: This control currently only supports horizontal scrolling and orientation.
/// </summary>
public class ScrollFreezePanel : Panel, IScrollInfo
{
#region [ Constants ]
 
//constant for amount of scrolling when using keyboard or mouse wheel
private const double LineSize = 10;
 
#endregion
 
#region [ Private Fields ]
 
private Point _offset;
private Size _extent = new Size(0, 0);
private Size _viewport = new Size(0, 0);
 
#endregion
 
#region [ Attached Properties ]
 
/// <summary>
/// Orientation
/// </summary>
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.RegisterAttached(
"Orientation",
typeof(Orientation),
typeof(ScrollFreezePanel),
new FrameworkPropertyMetadata(Orientation.Horizontal));
 
/// <summary>
/// Gets the Orientation property
/// </summary>
public static Orientation GetOrientation(DependencyObject d)
{
return (Orientation)d.GetValue(OrientationProperty);
}
 
/// <summary>
/// Sets the Orientation property
/// </summary>
public static void SetOrientation(DependencyObject d, Orientation value)
{
d.SetValue(OrientationProperty, value);
}
 
/// <summary>
/// ScrollBehaviour
/// </summary>
public static readonly DependencyProperty ScrollBehaviourProperty =
DependencyProperty.RegisterAttached(
"ScrollBehaviour",
typeof(ScrollBehaviours),
typeof(ScrollFreezePanel),
new FrameworkPropertyMetadata(ScrollBehaviours.Scrollable));
 
/// <summary>
/// Gets the ScrollBehaviour property
/// </summary>
public static ScrollBehaviours GetScrollBehaviour(DependencyObject d)
{
return (ScrollBehaviours)d.GetValue(ScrollBehaviourProperty);
}
 
/// <summary>
/// Sets the ScrollBehaviour property
/// </summary>
public static void SetScrollBehaviour(DependencyObject d, ScrollBehaviours value)
{
d.SetValue(ScrollBehaviourProperty, value);
}
 
#endregion
 
#region [ Overriden Methods ]
 
/// <summary>
/// Positions child elements and determines a size for the panel
/// </summary>
/// <param name="arrangeSize">The final area within the parent that this element
/// should use to arrange itself and its children.</param>
/// <returns>The actual size used.</returns>
protected override Size ArrangeOverride(Size arrangeSize)
{
var finalRect = new Rect(arrangeSize);
if (IsHorizontal)
finalRect.X -= _offset.X;
else
finalRect.Y -= _offset.Y;
 
double width = 0.0;
double height = 0.0;
double offSetLeftUsed = 0;
double offSetTopUsed = 0;
Rect[] elementsOffsetRight = ScrollFreezeCalculateInFront(Children);
 
for (int i = 0; i < Children.Count; i++)
{
UIElement element = Children[i];
if (IsHorizontal)
finalRect.X += width;
else
finalRect.Y += height;
 
width = element.DesiredSize.Width;
height = element.DesiredSize.Height;
 
finalRect.Width = IsHorizontal ? width : Math.Max(arrangeSize.Width, element.DesiredSize.Width);
finalRect.Height = IsHorizontal ? Math.Max(arrangeSize.Height, element.DesiredSize.Height) : height;
 
switch (GetScrollBehaviour(element))
{
case ScrollBehaviours.NonScrollable:
if (IsHorizontal ?
(finalRect.X + width) >= (_viewport.Width - elementsOffsetRight[i + 1].X)
: (finalRect.Y + height) >= (_viewport.Height - elementsOffsetRight[i + 1].Y)
)
{
SetZIndex(element, 1);
Rect tempVisible = finalRect;
if (IsHorizontal)
tempVisible.X = _viewport.Width - elementsOffsetRight[i].X;
else
tempVisible.Y = _viewport.Height - elementsOffsetRight[i].Y;
 
element.Arrange(tempVisible);
}
else
{
SetZIndex(element, 1);
Rect temp = finalRect;
if (IsHorizontal)
temp.X += _offset.X;
else
temp.Y += _offset.Y;
 
element.Arrange(temp);
 
offSetLeftUsed += width;
offSetTopUsed += height;
}
break;
 
case ScrollBehaviours.AlwaysVisible:
if (IsHorizontal ? finalRect.X <= offSetLeftUsed : finalRect.Y <= offSetTopUsed)
{
SetZIndex(element, 1);
Rect tempVisible = finalRect;
 
if (IsHorizontal)
tempVisible.X = offSetLeftUsed;
else
tempVisible.Y = offSetTopUsed;
 
element.Arrange(tempVisible);
offSetLeftUsed += width;
offSetTopUsed += height;
}
else if (IsHorizontal ?
(finalRect.X + width) >= (_viewport.Width - elementsOffsetRight[i + 1].X)
: (finalRect.Y + height) >= (_viewport.Height - elementsOffsetRight[i + 1].Y)
)
{
SetZIndex(element, 1);
Rect tempVisible = finalRect;
if (IsHorizontal)
tempVisible.X = _viewport.Width - elementsOffsetRight[i].X;
else
tempVisible.Y = _viewport.Height - elementsOffsetRight[i].Y;
 
element.Arrange(tempVisible);
}
else
{
SetZIndex(element, 0);
element.Arrange(finalRect);
}
break;
 
default:
SetZIndex(element, 0);
element.Arrange(finalRect);
break;
}
}
 
return arrangeSize;
}
 
/// <summary>
/// Measures the size in layout required for child elements and determines
/// a size for the panel
/// </summary>
/// <param name="availableSize">The available size that this element can give
///  to child elements. Infinity can be specified as a value to indicate that the
///  element will size to whatever content is available.</param>
/// <returns>The size that this element determines it needs during layout, based on
/// its calculations of child element sizes.</returns>
protected override Size MeasureOverride(Size availableSize)
{
var size = new Size();
 
for (int i = 0; i < InternalChildren.Count; i++)
{
UIElement element = InternalChildren[i];
element.Measure(availableSize);
Size desiredSize = element.DesiredSize;
 
if (IsHorizontal)
{
size.Width += desiredSize.Width;
size.Height = Math.Max(size.Height, desiredSize.Height);
}
else
{
size.Width = Math.Max(size.Width, desiredSize.Width);
size.Height += desiredSize.Height;
}
}
 
UpdateScrollInfo(availableSize, size);
 
return size;
}
 
#endregion
 
#region [ Private Methods ]
 
//calculate the horizontal offset for each element in a collection based on
//the ScrollBehaviour attached property
private static Rect[] ScrollFreezeCalculateInFront(UIElementCollection children)
{
var elementsOffsetRight = new Rect[children.Count + 1];
 
var calculated = new Rect();
 
for (int i = children.Count - 1; i >= 0; i--)
{
UIElement element = children[i];
ScrollBehaviours behaviour = GetScrollBehaviour(element);
if (behaviour == ScrollBehaviours.AlwaysVisible ||
behaviour == ScrollBehaviours.NonScrollable)
{
calculated.X += element.DesiredSize.Width;
calculated.Y += element.DesiredSize.Height;
}
elementsOffsetRight[i] = calculated;
}
 
return elementsOffsetRight;
}
 
//update scroll info based on availble and actual sizes
private void UpdateScrollInfo(Size available, Size actual)
{
if (IsHorizontal ? available.Width < actual.Width : available.Height < actual.Height)
{
_viewport = available;
_extent = actual;
 
if (ScrollOwner != null)
{
ScrollOwner.InvalidateScrollInfo();
}
else
{
_offset = new Point();
}
}
else
{
_viewport = actual;
_extent = new Size(0, 0);
_offset = new Point();
}
}
 
#endregion
 
#region [ IScrollInfo Properties ]
 
/// <summary>
/// Gets or sets a value that indicates whether scrolling on the horizontal
/// axis is possible.
/// </summary>
public bool CanHorizontallyScroll
{
get;
set;
}
 
/// <summary>
/// Gets or sets a value that indicates whether scrolling on the vertical
/// axis is possible. This will always return false.
/// </summary>
public bool CanVerticallyScroll
{
get;
set;
}
 
/// <summary>
/// Gets or sets a ScrollViewer  element that controls scrolling behavior.
/// </summary>
public ScrollViewer ScrollOwner
{
get;
set;
}
 
/// <summary>
/// Gets the horizontal offset of the scrolled content.
/// </summary>
public double HorizontalOffset
{
get return _offset.X; }
}
 
/// <summary>
/// Gets the vertical offset of the scrolled content.
/// </summary>
public double VerticalOffset
{
get return _offset.Y; }
}
 
/// <summary>
/// Gets the vertical size of the extent.
/// </summary>
public double ExtentHeight
{
get return _extent.Height; }
}
 
/// <summary>
/// Gets the horizontal size of the extent.
/// </summary>
public double ExtentWidth
{
get return _extent.Width; }
}
 
/// <summary>
/// Gets the vertical size of the viewport for this content.
/// </summary>
public double ViewportHeight
{
get return _viewport.Height; }
}
 
/// <summary>
/// Gets the horizontal size of the viewport for this content.
/// </summary>
public double ViewportWidth
{
get return _viewport.Width; }
}
 
#endregion
 
#region [ IScrollInfo Methods ]
 
/// <summary>
/// Sets the amount of horizontal offset.
/// </summary>
/// <param name="offset">The degree to which content is horizontally offset from the
/// containing viewport.</param>
public void SetHorizontalOffset(double offset)
{
if (offset < 0 || _viewport.Width >= _extent.Width)
{
offset = 0;
}
else if (offset + _viewport.Width >= _extent.Width)
{
offset = _extent.Width - _viewport.Width;
}
 
_offset.X = offset;
 
if (ScrollOwner != null)
{
ScrollOwner.InvalidateScrollInfo();
}
 
InvalidateArrange();
}
 
/// <summary>
/// Scrolls left within content by one logical unit.
/// </summary>
public void LineLeft()
{
SetHorizontalOffset(HorizontalOffset - LineSize);
}
 
/// <summary>
/// Scrolls right within content by one logical unit.
/// </summary>
public void LineRight()
{
SetHorizontalOffset(HorizontalOffset + LineSize);
}
 
/// <summary>
/// Scrolls left within content after a user clicks the wheel button on a mouse.
/// </summary>
public void MouseWheelLeft()
{
LineLeft();
}
 
/// <summary>
/// Scrolls right within content after a user clicks the wheel button on a mouse.
/// </summary>
public void MouseWheelRight()
{
LineRight();
}
 
/// <summary>
/// Scrolls down within content after a user scrolls down the wheel on a mouse.
/// In Horizontal orientation will scroll content RIGHT
/// </summary>
public void MouseWheelDown()
{
LineDown();
}
 
/// <summary>
/// Scrolls up within content after a user scrolls up the wheel on a mouse.
/// In Horizontal orientation will scroll content LEFT
/// </summary>
public void MouseWheelUp()
{
LineUp();
}
 
/// <summary>
/// Scrolls down within content by one logical unit.
/// </summary>
public void LineDown()
{
SetHorizontalOffset(HorizontalOffset + LineSize);
}
 
/// <summary>
/// Scrolls up within content by one logical unit.
/// </summary>
public void LineUp()
{
SetHorizontalOffset(HorizontalOffset - LineSize);
}
 
/// <summary>
/// Scrolls down within content by one page.
/// </summary>
public void PageDown()
{
SetHorizontalOffset(HorizontalOffset + LineSize * 10);
}
 
/// <summary>
/// Scrolls left within content by one page.
/// NOT IMPLEMENTED
/// </summary>
public void PageLeft()
{
}
 
/// <summary>
/// Scrolls right within content by one page.
/// NOT IMPLEMENTED
/// </summary>
public void PageRight()
{
}
 
/// <summary>
/// Scrolls up within content by one page.
/// </summary>
public void PageUp()
{
SetHorizontalOffset(HorizontalOffset - LineSize * 10);
}
 
/// <summary>
/// Forces content to scroll until the coordinate space of a Visual object is visible.
/// NOT IMPLEMENTED
/// </summary>
/// <param name="visual">A Visual that becomes visible.</param>
/// <param name="rectangle">A bounding rectangle that identifies the coordinate space to
///  make visible.</param>
/// <returns>A Rect that is visible.</returns>
public Rect MakeVisible(Visual visual, Rect rectangle)
{
return rectangle;//TODO:IMPLEMENT
}
 
/// <summary>
/// Sets the amount of vertical offset.
/// </summary>
/// <param name="offset">The degree to which content is vertically offset from the
///  containing viewport.</param>
public void SetVerticalOffset(double offset)
{
if (offset < 0 || _viewport.Height >= _extent.Height)
{
offset = 0;
}
else if (offset + _viewport.Height >= _extent.Height)
{
offset = _extent.Height - _viewport.Height;
}
 
_offset.Y = offset;
 
if (ScrollOwner != null)
{
ScrollOwner.InvalidateScrollInfo();
}
 
InvalidateArrange();
}
 
#endregion
 
#region [ Private Properties ]
private bool IsHorizontal { get return GetOrientation(this) == Orientation.Horizontal; } }
#endregion
}
}


Here is a screenshot of the panel in action:

The above window sample is from the following XAML:



<Window x:Class="WpfApplication1.MainWindow"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Panels="clr-namespace:Iimaginec.Lib.Controls.Panels"
Title="MainWindow" Height="850" Width="800">
<StackPanel>
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Visible" Width="280" Height="100">
<Panels:ScrollFreezePanel Orientation="Horizontal">
<Button Content="Non Scrollable" Width="100" Height="30" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
<Button Content="Normal 1" Width="100" Height="30" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="30" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="30" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="30" Background="RosyBrown"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Visible" Width="280" Height="100">
<Panels:ScrollFreezePanel Orientation="Horizontal">
<Button Content="Normal 1" Width="100" Height="30" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="30" Background="Aqua"/>
<Button Content="Always Visible" Width="100" Height="30" Panels:ScrollFreezePanel.ScrollBehaviour="AlwaysVisible" Background="Green"/>
<Button Content="Normal 3" Width="100" Height="30" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="30" Background="RosyBrown"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Visible" Width="280" Height="100">
<Panels:ScrollFreezePanel Orientation="Horizontal">
<Button Content="Normal 1" Width="100" Height="30" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="30" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="30" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="30" Background="RosyBrown"/>
<Button Content="Non Scrollable" Width="100" Height="30" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Visible" Width="280" Height="100">
<Panels:ScrollFreezePanel Orientation="Horizontal">
<Button Content="Normal 1" Width="100" Height="30" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="30" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="30" Background="Red"/>
<Button Content="Always Visible" Width="100" Height="30" Panels:ScrollFreezePanel.ScrollBehaviour="AlwaysVisible" Background="Green"/>
<Button Content="Normal 4" Width="100" Height="30" Background="RosyBrown"/>
<Button Content="Non Scrollable" Width="100" Height="30" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" HorizontalScrollBarVisibility="Visible" Width="280" Height="100">
<Panels:ScrollFreezePanel Orientation="Horizontal">
<Button Content="Non Scrollable" Width="100" Height="30" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
<Button Content="Normal 1" Width="100" Height="30" Background="Yellow"/>
<Button Content="Always Visible" Width="100" Height="30" Panels:ScrollFreezePanel.ScrollBehaviour="AlwaysVisible" Background="Green"/>
<Button Content="Normal 2" Width="100" Height="30" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="30" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="30" Background="RosyBrown"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
 
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<ScrollViewer CanContentScroll="True" Width="100" Height="280">
<Panels:ScrollFreezePanel Orientation="Vertical">
<Button Content="Non Scrollable" Width="100" Height="100" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
<Button Content="Normal 1" Width="100" Height="100" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="100" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="100" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="100" Background="RosyBrown"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" Width="100" Height="280">
<Panels:ScrollFreezePanel Orientation="Vertical">
<Button Content="Normal 1" Width="100" Height="100" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="100" Background="Aqua"/>
<Button Content="Always Visible" Width="100" Height="100" Panels:ScrollFreezePanel.ScrollBehaviour="AlwaysVisible" Background="Green"/>
<Button Content="Normal 3" Width="100" Height="100" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="100" Background="RosyBrown"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" Width="100" Height="280">
<Panels:ScrollFreezePanel Orientation="Vertical">
<Button Content="Normal 1" Width="100" Height="100" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="100" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="100" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="100" Background="RosyBrown"/>
<Button Content="Non Scrollable" Width="100" Height="100" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" Width="100" Height="280">
<Panels:ScrollFreezePanel Orientation="Vertical">
<Button Content="Normal 1" Width="100" Height="100" Background="Yellow"/>
<Button Content="Normal 2" Width="100" Height="100" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="100" Background="Red"/>
<Button Content="Always Visible" Width="100" Height="100" Panels:ScrollFreezePanel.ScrollBehaviour="AlwaysVisible" Background="Green"/>
<Button Content="Normal 4" Width="100" Height="100" Background="RosyBrown"/>
<Button Content="Non Scrollable" Width="100" Height="100" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
<ScrollViewer CanContentScroll="True" Width="100" Height="280">
<Panels:ScrollFreezePanel Orientation="Vertical">
<Button Content="Non Scrollable" Width="100" Height="100" Panels:ScrollFreezePanel.ScrollBehaviour="NonScrollable" Background="Gray"/>
<Button Content="Normal 1" Width="100" Height="100" Background="Yellow"/>
<Button Content="Always Visible" Width="100" Height="100" Panels:ScrollFreezePanel.ScrollBehaviour="AlwaysVisible" Background="Green"/>
<Button Content="Normal 2" Width="100" Height="100" Background="Aqua"/>
<Button Content="Normal 3" Width="100" Height="100" Background="Red"/>
<Button Content="Normal 4" Width="100" Height="100" Background="RosyBrown"/>
</Panels:ScrollFreezePanel>
</ScrollViewer>
</StackPanel>
</StackPanel>
</Window>

출처1

https://iimaginec.wordpress.com/2010/06/19/scroll-freeze-panel-in-wpf/

출처2