How to create a slider with a non-linear scale?
I have a slider with a minimum value of 0 and maximum of 500.
I want to when the slider goes to 100, t开发者_JAVA技巧he thumb be in the middle of the slider.
I know it seems wierd, but some programs do it with zoom slider, and I believe it's better.
A good formula for the displayed value is a monotonous function such as a power curve, in the following form:
DisplayValue = A + B * Math.Exp(C * SliderValue);
The internal slider value (from 0 to 1 for instance) is obtained by inverting the formula:
SliderValue = Math.Log((DisplayValue - A) / B) / C;
Now how to obtain A, B and C? By using the three constraints you gave:
f(0.0) = 0
f(0.5) = 100
f(1.0) = 500
Three equations, three unknowns, this is solved using basic maths:
A + B = 0
A + B exp(C * 0.5) = 100
A + B exp(C) = 500
B (exp(C * 0.5) - 1) = 100
B (exp(C) - 1) = 500
exp(C) - 5 exp(C * 0.5) + 4 = 0 // this is a quadratic equation
exp(C * 0.5) = 4
C = log(16)
B = 100/3
A = -100/3
Yielding the following code:
double B = 100.0 / 3;
double C = Math.Log(16.0);
DisplayValue = B * (Math.Exp(C * SliderValue) - 1.0);
You can see that the display value is at 100 when the internal value is in the middle:
Edit: since a generic formula was requested, here it is. Given:
f(0.0) = x
f(0.5) = y
f(1.0) = z
The values for A, B and C are:
A = (xz - y²) / (x - 2y + z)
B = (y - x)² / (x - 2y + z)
C = 2 * log((z-y) / (y-x))
Note that if x - 2y + z
or y - x
is zero, there is no solution and you’ll get a division by zero. That’s because in this case, the scale is actually linear. You need to take care of that special case.
let the slider as it is and use a ValueConverter for your bindings. In the ValueConverter use the non-linear scaling to scale the value as you wish.
Just as a further reference; if you are not interested on exact positions for your slider to correspond to specific values in your scale but still want a behavior where the slider is more sensitive to values on the beginning of the scale than on the end, then perhaps using a simple log scale may suffice.
public class LogScaleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double x = (int)value;
return Math.Log(x);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
double x = (double)value;
return (int)Math.Exp(x);
}
}
Based on the algorithm from sam hocevar, here is the code I put together :
/// <summary>
/// Scale a linear range between 0.0-1.0 to an exponential scale using the equation returnValue = A + B * Math.Exp(C * inputValue);
/// </summary>
/// <param name="inoutValue">The value to scale</param>
/// <param name="midValue">The value returned for input value of 0.5</param>
/// <param name="maxValue">The value to be returned for input value of 1.0</param>
/// <returns></returns>
private double ExpScale(double inputValue, double midValue, double maxValue)
{
double returnValue = 0;
if (inputValue < 0 || inputValue > 1) throw new ArgumentOutOfRangeException("Input value must be between 0 and 1.0");
if (midValue <= 0 || midValue >= maxValue) throw new ArgumentOutOfRangeException("MidValue must be greater than 0 and less than MaxValue");
// returnValue = A + B * Math.Exp(C * inputValue);
double M = maxValue / midValue;
double C = Math.Log(Math.Pow(M - 1, 2));
double B = maxValue / (Math.Exp(C) - 1);
double A = -1 * B;
returnValue = A + B * Math.Exp(C * inputValue);
return returnValue;
}
This was such an interesting question that I couldn't leave it alone, and hopefully I got what you're asking right :)
You want to change the Value
of a Slider
from Linear to a Quadratic Function by specifying the Y value of the function when the Thumb is in the middle.
A Quadratic Function is written on the form
Since we have 3 points, we have 3 sets of values for X and Y.
(X1, Y1) = 0, 0
(X2, Y2) = MiddleX, CenterQuadraticValue (in your case 100)
(X3, Y3) = Maximum, Maximum (in your case 500)
From here, we can create a Quadratic Equation (see this link for example) which comes out to
Unfortunately, some values in this graph ends up below 0 so they will have to be coerced to 0 (I included a graph in the bottom of the answer).
I created a control, QuadraticSlider
, which derives from Slider
and adds two Dependency Properties: QuadraticValue
and CenterQuadraticValue
. It calculates QuadraticValue
using the formula above based on Value
, Maximum
, Minimum
and CenterQuadraticValue
. It also does the reverse: setting QuadraticValue
updates Value
. So instead of Binding to Value
, bind to QuadraticValue
.
Edit: The last version was a little buggy. Fixed a couple of things
- Calculating
Value
fromQuadraticValue
no longer breaks when "a" is 0 - Used wrong root from the second degree solution when the derivate was negative
I uploaded a sample application where QuadraticSlider
is used to zoom a picture. All parameteres can be specified and the first picture uses Value
and the other QuadraticValue
.
Download it here if you want to try it out.
It looks like this
And this is what the graph looks like, notice the values below 0
Some addition to Meleak's post. I've slightly corrected QuadraticSlider. There was issue with event handlers (event on QuadraticValueChanged with yet prevoius value; event during initialization with out of range [min, max] value).
protected override void OnValueChanged(double oldValue, double newValue)
{
QuadraticValue = a * Math.Pow(Value, 2) + b * Value + c;
base.OnValueChanged(oldValue, newValue);
}
public double QuadraticValue
{
get {
var qv = (double)GetValue(QuadraticValueProperty);
if (double.IsNaN(qv))
qv = 0;
qv = Math.Max(qv, base.Minimum);
qv = Math.Min(qv, base.Maximum);
return qv;
}
set
{
SetValue(QuadraticValueProperty, value);
}
}
To generalise on Sam Hocevar's excellent answer:
Let the intended Maximum value be M.
Let the value at the slider midpoint be m.
(obviously, 0 < m < M), then
A = - M*m^2 / (M^2 - 2*m*M)
B = M*m^2 / (M^2 - 2*m*M)
C = Ln((M - m)^2 / m^2) // <- logarithm to the base of e, I always think of 'Log' as base 10
One must take care to treat the case 2*m=M seperately, because that leads to a division by 0. But in that case, you'd have the slider behave in a linear fashion anyway.
Chosing m from between M/2 and M makes for a logarithmic curve: The effective slider values rise fast at first, then slowly later on. This basically reverses the effect and gives the user finer control of the higher values.
As mentioned, an m close to M/2 makes the slider basically linear.
Choosing m close to 0 or close to M makes for fine control over the very low or the very high values.
I suppose one could use this in combination with a second slider that sets m to a value between 0 and M to change the ... errr ... sensitive zone of the real slider.
Not nearly as elegant as some of the other answers here but instead of a smooth curve you could just use two straight lines. With y as your slider's position and x as your actual value, you'd use (y1 - y2)/(x1 - x2)
to get the slope, then y - y1 = slope * (x - x1)
to get the y intercept. Do that for each half of the slider then use a conditional statement to treat values differently depending on whether they are above or below 50. The basic maths in your case:
(0 - 50)/(0 - 100)
-50/-100
1/2
y = x/2
...since we know the y intercept is 0. Then...
(50-100)/(100-500)
-50/-400
1/8
y - 50 = 1/8 * (x - 100)
y - 50 = x/8 - 25/2
y = x/8 + 75/2
From a user experience perspective, you're definitely better off using a curve (I especially like Cesar's log idea) because it avoids a sharp "elbow" where the slider starts suddenly increasing values at a totally different rate. But this approach is simple to implement and flexible (you could easily add more zones with different ratios) so I thought it was worth mentioning it anyway.
Here is the go code for deriving the exponential equation: https://play.golang.org/p/JlWlwZjoebE
精彩评论