如何在 WPF 中模拟悬挂电缆?
我有一个非常“基于连接”的应用程序,即多个输入/输出。
“电缆”的 UI 概念正是我所寻找的,以便让用户清楚地了解该概念。 Propellerhead 在其音频组件 Reason 软件中采用了类似的方法,如这段 YouTube 视频(快进至2m:50s)。
我可以通过绘制从 A 点到 B 点的样条线来使这个概念在 GDI 中发挥作用,必须有一种更优雅的方法来使用路径或 WPF 中的某些东西来实现此目的,但是您从哪里开始呢?有没有好方法来模拟抓住电缆并摇动时电缆摆动的动画?
如果这个轮子已经为 WPF 发明了,我也愿意接受控制库(商业或开源)。
更新:感谢到目前为止答案中的链接,我快要完成了。
我以编程方式创建了一条 BezierCurve
,其中点 1 为 (0 , 0)
,点 2 是底部“悬挂”点,点 3 是鼠标光标所在的位置。我为 Point 2 创建了一个 PointAnimation
,并对其应用了 ElasticEase
缓动函数,以提供“摆动”效果(即,稍微弹动中间点)。
唯一的问题是,动画似乎运行得有点晚。每次鼠标移动时我都会启动故事板,有更好的方法来制作这个动画吗?到目前为止,我的解决方案位于此处:
代码:
private Path _path = null;
private BezierSegment _bs = null;
private PathFigure _pFigure = null;
private Storyboard _sb = null;
private PointAnimation _paPoint2 = null;
ElasticEase _eEase = null;
private void cvCanvas_MouseMove(object sender, MouseEventArgs e)
{
var position = e.GetPosition(cvCanvas);
AdjustPath(position.X, position.Y);
}
// basic idea: when mouse moves, call AdjustPath and draw line from (0,0) to mouse position with a "hang" in the middle
private void AdjustPath(double x, double y)
{
if (_path == null)
{
_path = new Path();
_path.Stroke = new SolidColorBrush(Colors.Blue);
_path.StrokeThickness = 2;
cvCanvas.Children.Add(_path);
_bs = new BezierSegment(new Point(0, 0), new Point(0, 0), new Point(0, 0), true);
PathSegmentCollection psCollection = new PathSegmentCollection();
psCollection.Add(_bs);
_pFigure = new PathFigure();
_pFigure.Segments = psCollection;
_pFigure.StartPoint = new Point(0, 0);
PathFigureCollection pfCollection = new PathFigureCollection();
pfCollection.Add(_pFigure);
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures = pfCollection;
_path.Data = pathGeometry;
}
double bottomOfCurveX = ((x / 2));
double bottomOfCurveY = (y + (x * 1.25));
_bs.Point3 = new Point(x, y);
if (_sb == null)
{
_paPoint2 = new PointAnimation();
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_paPoint2.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
_eEase = new ElasticEase();
_paPoint2.EasingFunction = _eEase;
_sb = new Storyboard();
Storyboard.SetTarget(_paPoint2, _path);
Storyboard.SetTargetProperty(_paPoint2, new PropertyPath("Data.Figures[0].Segments[0].Point2"));
_sb.Children.Add(_paPoint2);
_sb.Begin(this);
}
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_sb.Begin(this);
}
I have an application that is very "connection-based", i.e. multiple inputs/outputs.
The UI concept of a "cable" is exactly what I'm looking for to make the concept clear to the user. Propellerhead took a similar approach in their Reason software for audio components, illustrated in this YouTube video (fast forward to 2m:50s).
I can make this concept work in GDI by painting a spline from point A to point B, there's got to be a more elegant way to use Paths or something in WPF for this, but where do you start? Is there a good way to simulate the animation of the cable swing when you grab it and shake it?
I'm also open to control libraries (commercial or open source) if this wheel has already been invented for WPF.
Update: Thanks to the links in the answers so far, I'm almost there.
I've created a BezierCurve
programmatically, with Point 1 being (0, 0)
, Point 2 being the bottom "hang" point, and Point 3 being wherever the mouse cursor is. I've created a PointAnimation
for Point 2 with an ElasticEase
easing function applied to it to give the "Swinging" effect (i.e., bouncing the middle point around a bit).
Only problem is, the animation seems to run a little late. I'm starting the Storyboard each time the mouse moves, is there a better way to do this animation? My solution so far is located here:
Code:
private Path _path = null;
private BezierSegment _bs = null;
private PathFigure _pFigure = null;
private Storyboard _sb = null;
private PointAnimation _paPoint2 = null;
ElasticEase _eEase = null;
private void cvCanvas_MouseMove(object sender, MouseEventArgs e)
{
var position = e.GetPosition(cvCanvas);
AdjustPath(position.X, position.Y);
}
// basic idea: when mouse moves, call AdjustPath and draw line from (0,0) to mouse position with a "hang" in the middle
private void AdjustPath(double x, double y)
{
if (_path == null)
{
_path = new Path();
_path.Stroke = new SolidColorBrush(Colors.Blue);
_path.StrokeThickness = 2;
cvCanvas.Children.Add(_path);
_bs = new BezierSegment(new Point(0, 0), new Point(0, 0), new Point(0, 0), true);
PathSegmentCollection psCollection = new PathSegmentCollection();
psCollection.Add(_bs);
_pFigure = new PathFigure();
_pFigure.Segments = psCollection;
_pFigure.StartPoint = new Point(0, 0);
PathFigureCollection pfCollection = new PathFigureCollection();
pfCollection.Add(_pFigure);
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures = pfCollection;
_path.Data = pathGeometry;
}
double bottomOfCurveX = ((x / 2));
double bottomOfCurveY = (y + (x * 1.25));
_bs.Point3 = new Point(x, y);
if (_sb == null)
{
_paPoint2 = new PointAnimation();
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_paPoint2.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
_eEase = new ElasticEase();
_paPoint2.EasingFunction = _eEase;
_sb = new Storyboard();
Storyboard.SetTarget(_paPoint2, _path);
Storyboard.SetTargetProperty(_paPoint2, new PropertyPath("Data.Figures[0].Segments[0].Point2"));
_sb.Children.Add(_paPoint2);
_sb.Begin(this);
}
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_sb.Begin(this);
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
如果您想要真正的动态运动(即,当您“摇动”鼠标指针时,您可以创建沿着绳索传播的波),您将需要使用有限元技术。但是,如果您可以接受静态行为,则可以简单地使用贝塞尔曲线。
首先,我将简要描述有限元方法,然后详细介绍静态方法。
动态方法
将“绳索”划分为大量(1000 个左右)“元素”,每个元素都有一个位置和速度向量。使用 CompositionTarget.Rendering 事件计算每个元素的位置,如下所示:
计算每个元素沿“绳索”从相邻元素受到的拉力,该拉力与元素之间的距离成正比。假设绳索本身没有质量。
计算每个“元素”上的净力矢量,其中包括沿绳索的每个相邻元素的拉力,加上恒定的重力。
使用质量常数将力矢量转换为加速度,并使用运动方程更新位置和速度。
使用带有 BeginFigure 和 PolyLineTo 的 StreamGeometry 构建来绘制线条。有了如此多的点,就没有理由进行额外的计算来创建三次贝塞尔曲线近似。
静态方法
将绳索分成大约 30 段,每段都是悬链线 y = a cosh(x/a) 的三次贝塞尔近似。您的末端控制点应位于悬链线曲线上,平行线应与悬链线相切,并且控制线长度根据悬链线的二阶导数设置。
在这种情况下,您可能还想渲染 StreamGeometry,使用 BeginFigure 和 PolyBezierTo 来构建它。
我会将其实现为类似于矩形和椭圆形的自定义形状子类“悬链线”。在这种情况下,您只需重写 DefineingGeometry 属性即可。为了提高效率,我还将重写 CacheDefiningGeometry、GetDefiningGeometryBounds 和 GetNaturalSize。
您首先需要决定如何参数化悬链线,然后为所有参数添加 DependencyProperties。确保在 FrameworkPropertyMetadata 中设置 AffectsMeasure 和 AffectsRender 标志。
一种可能的参数化是 XOffset、YOffset、Length。另一个可能是 XOffset、YOffset、SagRelativeToWidth。这取决于最容易绑定的内容。
定义 DependencyProperties 后,实现 DefiningGeometry 属性来计算三次贝塞尔曲线控制点、构造 StreamGeometry 并返回它。
如果这样做,您可以将悬链线控件放置在任何地方并获得悬链线曲线。
If you want true dynamic motion (ie, when you "shake" the mouse pointer you can create waves that travel along the cord), you will need to use finite element techniques. However if you are ok with static behavior you can simply use Bezier curves.
First I'll briefly describe the finite element approach, then go into more detail on the static approach.
Dynamic approach
Divide your "cord" into a large number (1000 or so) "elements", each with a position and velocity Vector. Use the CompositionTarget.Rendering event to compute each element position as follows:
Compute the pull on each element along the "cord" from adjacent elements, which is proportional to the distance between elements. Assume the cord itself is massless.
Compute the net force vector on each "element" which consists of the pull from each adjacent element along the cord, plus the constant force of gravity.
Use a mass constant to convert the force vector to accelaration, and update the position and velocity using the equations of motion.
Draw the line using a StreamGeometry build with a BeginFigure followed by a PolyLineTo. With so many points there is little reason to do the extra computations to create a cubic bezier approximation.
Static approach
Divide your cord into perhaps 30 segments, each of which is a cubic bezier approximation to the catenary y = a cosh(x/a). Your end control points should be on the catenary curve, the parallels should tangent to the catenaries, and the control line lengths set based on the second derivative of the catenary.
In this case you will probably also want to render a StreamGeometry, using BeginFigure and PolyBezierTo to build it.
I would implement this as a custom Shape subclass "Catenary" similar to Rectangle and Ellipse. In that case, all you have to override the DefiningGeometry property. For efficiency I would also override CacheDefiningGeometry, GetDefiningGeometryBounds, and GetNaturalSize.
You would first decide how to parameterize your catenary, then add DependencyProperties for all your parameters. Make sure you set the AffectsMeasure and AffectsRender flags in your FrameworkPropertyMetadata.
One possible parameterization would be XOffset, YOffset, Length. Another might be XOffset, YOffset, SagRelativeToWidth. It would depend on what would be easiest to bind to.
Once your DependencyProperties are defined, implement your DefiningGeometry property to compute the cubic bezier control points, construct the StreamGeometry, and return it.
If you do this, you can drop a Catenary control anywhere and get a catenary curve.
路径中的用户贝塞尔曲线段。
http://www.c-sharpcorner.com/UploadFile/dbeniwal321/WPFBezier01302009015211AM /WPFBezier.aspx
User bezier curve segments in a path.
http://www.c-sharpcorner.com/UploadFile/dbeniwal321/WPFBezier01302009015211AM/WPFBezier.aspx
恕我直言,“悬挂”(物理模拟)电缆是一种过度使用的情况 - 注重外观而不是可用性。
您确定您这样做不会扰乱用户体验吗?
在基于节点/连接的 UI 中,我发现清晰连接(例如在 Quartz Composer 中:http://ellington.tvu.ac.uk/ma/wp-content/uploads/2006/05/images/Quartz%20Composer_screenshot_011.png< /a> )比像摆动电缆那样引人注目的东西更重要,这些电缆的方向与实际连接点的方向不同(由于重力而向下)。 (同时消耗 CPU 周期进行模拟,这在其他地方可能更有用)
只是我的 0.02 美元
IMHO 'hanging' (physically simulated) cables are a case of over-doing it - favouring looks over usability.
Are you sure you're not just cluttering the user-experience ?
In a node/connection-based UI I find clear connections (like in Quartz Composer : http://ellington.tvu.ac.uk/ma/wp-content/uploads/2006/05/images/Quartz%20Composer_screenshot_011.png ) way more important than eye-candy like swinging cables that head in a different direction (down due to gravity) than where the actually connection-point is. (And in the mean time eat up CPU-cycles for the simulation that could be more useful elsewhere)
Just my $0.02