如何让画笔平滑中间没有线条
Posted
技术标签:
【中文标题】如何让画笔平滑中间没有线条【英文标题】:how to make the brush smooth without lines in the middle 【发布时间】:2015-02-16 23:06:00 【问题描述】:大家好, 正如您在之前的画笔中看到的,中间有线条, 它不是那么平滑 如何使它顺利? (如何删除该行) 我用混合创建它
<Grid x:Name="LayoutRoot">
<Grid.Background>
<LinearGradientBrush EndPoint="0.452,1.962" StartPoint="1.164,-0.352">
<GradientStop Color="#FF202020" Offset="0"/>
<GradientStop Color="#FF545454" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
</Grid>
【问题讨论】:
你能发布你创建画笔的代码吗?这是渐变画笔吗? 好的,你现在可以检查代码了。 【参考方案1】:条带是梯度算法的产物。它必须将该区域分成带子,每个带子都填充了稍微不同的颜色。边缘实际上是一种视觉错觉,其效果是使它们比您想象的更明显。要减少这种影响,您需要减小每个波段的宽度。
解决办法是:
-
填充较小的区域 - 每个条带都较窄。
增加波段数。加大两个极端之间的对比。
提高显示器的颜色分辨率。如果您有更多颜色可供选择,那么两种最终颜色之间的可用范围就会更大。
我确实意识到这些解决方案要么 a) 不可能,要么 b) 不切实际。这是一个你将不得不忍受的问题。
一种实用的解决方案可能是将画笔替换为在 Photoshop 或其他图像处理包中创建的图像。这可能会为您提供条带较少的图像 - 但是您会受到图像大小的限制 - 如果没有像素化,您将无法放大它。
【讨论】:
【参考方案2】:前段时间,我为我的 WPF 项目编写了平滑线性渐变。它消除了条带,但有两个警告:
不能用于数据绑定颜色或DynamicResource
。
它目前只支持垂直渐变,因为这就是我所需要的
它被实现为动态创建的ImageBrush
,使用Ordered Dithering。另外,它也是一个MarkupExtension
,因为不能简单地继承任何Brush
类(它们都是密封的)。
/// <summary>
/// Brush that lets you draw vertical linear gradient without banding.
/// </summary>
[MarkupExtensionReturnType(typeof(Brush))]
public class SmoothLinearGradientBrush : MarkupExtension
private static PropertyInfo dpiX_;
private static PropertyInfo dpiY_;
private static byte[,] bayerMatrix_ =
1, 9, 3, 11 ,
13, 5, 15, 7 ,
1, 9, 3, 11 ,
16, 8, 14, 6
;
static SmoothLinearGradientBrush()
dpiX_ = typeof(SystemParameters).GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);
dpiY_ = typeof(SystemParameters).GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static);
/// <summary>
/// Gradient color at the top
/// </summary>
public Color From get; set;
/// <summary>
/// Gradient color at the bottom
/// </summary>
public Color To get; set;
public override object ProvideValue(IServiceProvider serviceProvider)
//If user changes dpi/virtual screen height during applicaiton lifetime,
//wpf will scale the image up for us.
int width = 20;
int height = (int)SystemParameters.VirtualScreenHeight;
int dpix = (int)dpiX_.GetValue(null);
int dpiy = (int)dpiY_.GetValue(null);
int stride = 4 * ((width * PixelFormats.Bgr24.BitsPerPixel + 31) / 32);
//dithering parameters
double bayerMatrixCoefficient = 1.0 / (bayerMatrix_.Length + 1);
int bayerMatrixSize = bayerMatrix_.GetLength(0);
//Create pixel data of image
byte[] buffer = new byte[height * stride];
for (int line = 0; line < height; line++)
double scale = (double)line / height;
for (int x = 0; x < width * 3; x += 3)
//scaling of color
double blue = ((To.B * scale) + (From.B * (1.0 - scale)));
double green = ((To.G * scale) + (From.G * (1.0 - scale)));
double red = ((To.R * scale) + (From.R * (1.0 - scale)));
//ordered dithering of color
//source: http://en.wikipedia.org/wiki/Ordered_dithering
buffer[x + line * stride] = (byte)(blue + bayerMatrixCoefficient * bayerMatrix_[x % bayerMatrixSize, line % bayerMatrixSize]);
buffer[x + line * stride + 1] = (byte)(green + bayerMatrixCoefficient * bayerMatrix_[x % bayerMatrixSize, line % bayerMatrixSize]);
buffer[x + line * stride + 2] = (byte)(red + bayerMatrixCoefficient * bayerMatrix_[x % bayerMatrixSize, line % bayerMatrixSize]);
var image = BitmapSource.Create(width, height, dpix, dpiy, PixelFormats.Bgr24, null, buffer, stride);
image.Freeze();
var brush = new ImageBrush(image);
brush.Freeze();
return brush;
在资源字典中的使用:
<local:SmoothLinearGradientBrush x:Key="WindowBackgroundBrush"
From="StaticResource WindowBackgroundColorLight"
To="StaticResource WindowBackgroundColorDark" />
然后是控制风格:
<Style ...>
<Setter Property="Background" Value="StaticResource WindowBackgroundBrush" />
</Style>
【讨论】:
【参考方案3】:我的另一个答案中的一个廉价而肮脏的选择是这样的:
将渐变添加到容器,给它一个小的负边距,使其溢出一点,向渐变添加一个 BlurEffect,然后在父容器上打开 ClipToBounds。这样,梯度会以牺牲性能为代价变得更好。
但是,根据您的用例,这可能不可行。
例子:
<Grid Height="26" Margin="-5,0" ClipToBounds="True">
<Grid Margin="-5">
<Grid.Effect>
<BlurEffect Radius="6" />
</Grid.Effect>
<Grid.Background>
<LinearGradientBrush>
<GradientStop x:Name="GradientStop7" Color="Magenta" Offset="0.0" />
<GradientStop x:Name="GradientStop8" Color="DarkOrchid" Offset=".2" />
<GradientStop x:Name="GradientStop9" Color="Purple" Offset="1" />
</LinearGradientBrush>
</Grid.Background>
</Grid>
</Grid>
负梯度应该等于模糊半径,这样它就不会在边缘变得透明。
【讨论】:
【参考方案4】:基于 ghord 的解决方案,我实现了一个更通用的渐变。我将它实现为 FrameworkElement 中的动态图像。您可以指定所有四个角的颜色,它特别适用于非常浅色的渐变,没有任何条纹。即使使用高 DPI 显示器,图像的缩放方式也能产生完美像素的抖动。
/// <summary>
/// A class that draws a pixel-perfect, dithered gradient image
/// </summary>
public class DitheredGradientImage : FrameworkElement
public static readonly DependencyProperty TopLeftColorProperty =
DependencyProperty.Register(nameof(TopLeftColor), typeof(Color), typeof(DitheredGradientImage),
new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));
public static readonly DependencyProperty TopRightColorProperty =
DependencyProperty.Register(nameof(TopRightColor), typeof(Color), typeof(DitheredGradientImage),
new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));
public static readonly DependencyProperty BottomLeftColorProperty =
DependencyProperty.Register(nameof(BottomLeftColor), typeof(Color), typeof(DitheredGradientImage),
new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));
public static readonly DependencyProperty BottomRightColorProperty =
DependencyProperty.Register(nameof(BottomRightColor), typeof(Color), typeof(DitheredGradientImage),
new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.AffectsRender, OnColorsChanged));
public Color TopLeftColor
get return (Color)this.GetValue(TopLeftColorProperty);
set this.SetValue(TopLeftColorProperty, value);
public Color TopRightColor
get return (Color)this.GetValue(TopRightColorProperty);
set this.SetValue(TopRightColorProperty, value);
public Color BottomLeftColor
get return (Color)this.GetValue(BottomLeftColorProperty);
set this.SetValue(BottomLeftColorProperty, value);
public Color BottomRightColor
get return (Color)this.GetValue(BottomRightColorProperty);
set this.SetValue(BottomRightColorProperty, value);
public Color TopColor
get return (TopLeftColor == TopRightColor) ? TopLeftColor : default(Color);
set TopLeftColor = value; TopRightColor = value;
public Color BottomColor
get return (BottomLeftColor == BottomRightColor) ? BottomLeftColor : default(Color);
set BottomLeftColor = value; BottomRightColor = value;
public Color LeftColor
get return (TopLeftColor == BottomLeftColor) ? TopLeftColor : default(Color);
set TopLeftColor = value; BottomLeftColor = value;
public Color RightColor
get return (TopRightColor == BottomRightColor) ? TopRightColor : default(Color);
set TopRightColor = value; BottomRightColor = value;
public double DpiScaleX => VisualTreeHelper.GetDpi(this).DpiScaleX;
public double DpiScaleY => VisualTreeHelper.GetDpi(this).DpiScaleY;
public double DpiX => VisualTreeHelper.GetDpi(this).PixelsPerInchX;
public double DpiY => VisualTreeHelper.GetDpi(this).PixelsPerInchX;
private WriteableBitmap bitmap = null;
private int lastPixelWidth = 0;
private int lastPixelHeight = 0;
protected static void OnColorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
// Force redraw by resetting width and height
if (d is DitheredGradientImage di)
di.lastPixelWidth = 0;
di.lastPixelHeight = 0;
protected void Update(int pixelWidth, int pixelHeight)
// Check if bitmap needs recreation, because it it is either
// not initialized, or too small to fit the current element size
if ((bitmap == null) ||
(bitmap.PixelWidth < pixelWidth) ||
(bitmap.PixelHeight < pixelHeight))
// Oversize, so that continuous resizing doesn't lead to constant bitmap recreation
bitmap = new WriteableBitmap(pixelWidth + 256, pixelHeight + 256, DpiX, DpiY, PixelFormats.Bgra32, null);
// As the old bitmap is gone, reset values so it gets redrawn anyway
lastPixelWidth = 0;
lastPixelHeight = 0;
// The gradient needs to be drawn to the exact size required
if ((lastPixelWidth != pixelWidth) || (lastPixelHeight != pixelHeight))
// Draw a line-based test pattern to verify proper pixel scaling (or the lack of)
//DrawTestPattern(pixelWidth, pixelHeight);
// Draw the actual gradient
DrawGradient(pixelWidth, pixelHeight);
// Store values to avoid redrawing when nothing has changed
lastPixelWidth = pixelWidth;
lastPixelHeight = pixelHeight;
protected void DrawTestPattern(int pixelWidth, int pixelHeight)
try
// Reserve the back buffer for updates.
bitmap.Lock();
unsafe
// Get a pointer to the back buffer.
IntPtr pBackBuffer = bitmap.BackBuffer;
for (int y = 0; y < pixelHeight; y++)
for (int x = 0; x < pixelWidth; x++)
// Draw alternate black and white lines
uint colorData = (y % 2 == 0) ? 0xFF000000 : 0xFFFFFFFF;
*(uint*)(pBackBuffer + (y * bitmap.BackBufferStride) + (x * 4)) = colorData;
// Specify the area of the bitmap that changed.
bitmap.AddDirtyRect(new Int32Rect(0, 0, pixelWidth, pixelHeight));
finally
// Release the back buffer and make it available for display.
bitmap.Unlock();
private const byte matrixSize = 4;
private static readonly float[,] scaledMatrix = CalculateMatrix(matrixSize);
private static float[,] CalculateMatrix(byte n)
uint[,] uintMatrix = new uint[n, n];
uint divisor = uint.MaxValue;
for (ushort i = 0; i < n; i++)
for (ushort j = 0; j < n; j++)
uintMatrix[i, j] = BitReverse(8, BitInterleave((ushort)(i ^ j), i));
if ((uintMatrix[i, j] != 0) && (divisor > uintMatrix[i, j]))
divisor = uintMatrix[i, j];
float[,] floatMatrix = new float[n, n];
float coefficient = 1.0f / (uintMatrix.Length * divisor);
for (ushort i = 0; i < n; i++)
for (ushort j = 0; j < n; j++)
floatMatrix[i, j] = uintMatrix[i, j] * coefficient;
return floatMatrix;
/// <summary>
/// Reverse all bits
/// </summary>
private static uint BitReverse(byte bits, uint value)
uint left = (uint)1 << (bits - 1);
uint right = 1;
uint result = 0;
for (int i = (bits - 1); i >= 1; i -= 2)
result |= (value & left) >> i;
result |= (value & right) << i;
left >>= 1;
right <<= 1;
return result;
/// <summary>
/// Bitwise interleave of two 16-bit unsigned integers
/// </summary>
private static uint BitInterleave(ushort x, ushort y)
uint _x = x;
_x = (_x | (_x << 8)) & 0x00FF00FF;
_x = (_x | (_x << 4)) & 0x0F0F0F0F;
_x = (_x | (_x << 2)) & 0x33333333;
_x = (_x | (_x << 1)) & 0x55555555;
uint _y = y;
_y = (_y | (_y << 8)) & 0x00FF00FF;
_y = (_y | (_y << 4)) & 0x0F0F0F0F;
_y = (_y | (_y << 2)) & 0x33333333;
_y = (_y | (_y << 1)) & 0x55555555;
return (uint)(_x | (_y << 1));
/// <summary>
/// Draws the gradient
/// </summary>
protected void DrawGradient(int pixelWidth, int pixelHeight)
// Caching the color values is mandatory, JIT compiler doesn't seem to cache access
byte topLeftR = TopLeftColor.R, topLeftG = TopLeftColor.G, topLeftB = TopLeftColor.B, topLeftA = TopLeftColor.A;
byte topRightR = TopRightColor.R, topRightG = TopRightColor.G, topRightB = TopRightColor.B, topRightA = TopRightColor.A;
byte bottomLeftR = BottomLeftColor.R, bottomLeftG = BottomLeftColor.G, bottomLeftB = BottomLeftColor.B, bottomLeftA = BottomLeftColor.A;
byte bottomRightR = BottomRightColor.R, bottomRightG = BottomRightColor.G, bottomRightB = BottomRightColor.B, bottomRightA = BottomRightColor.A;
try
// Reserve the back buffer for updates.
bitmap.Lock();
unsafe
// Get a pointer to the back buffer.
IntPtr pBackBuffer = bitmap.BackBuffer;
int backBufferStride = bitmap.BackBufferStride;
for (int y = 0; y < pixelHeight; y++)
float incYScale = y / (float)pixelHeight;
float decYScale = 1 - (y / (float)pixelHeight);
for (int x = 0; x < pixelWidth; x++)
float incXScale = x / (float)pixelWidth;
float decXScale = 1 - (x / (float)pixelWidth);
float r = (((topLeftR * decXScale) + (topRightR * incXScale)) * decYScale) +
(((bottomLeftR * decXScale) + (bottomRightR * incXScale)) * incYScale);
float g = (((topLeftG * decXScale) + (topRightG * incXScale)) * decYScale) +
(((bottomLeftG * decXScale) + (bottomRightG * incXScale)) * incYScale);
float b = (((topLeftB * decXScale) + (topRightB * incXScale)) * decYScale) +
(((bottomLeftB * decXScale) + (bottomRightB * incXScale)) * incYScale);
float a = (((topLeftA * decXScale) + (topRightA * incXScale)) * decYScale) +
(((bottomLeftA * decXScale) + (bottomRightA * incXScale)) * incYScale);
byte* p = (byte*)(pBackBuffer + (y * backBufferStride) + (x * 4));
p[0] = (byte)(b + scaledMatrix[x % matrixSize, y % matrixSize]);
p[1] = (byte)(g + scaledMatrix[x % matrixSize, y % matrixSize]);
p[2] = (byte)(r + scaledMatrix[x % matrixSize, y % matrixSize]);
p[3] = (byte)a;
// Specify the area of the bitmap that changed.
bitmap.AddDirtyRect(new Int32Rect(0, 0, pixelWidth, pixelHeight));
finally
bitmap.Unlock();
protected override void OnRender(DrawingContext dc)
// Math.Round seems to be used in WPF, so it's important to use the
// same function, otherwise you sometimes don't get pixel-perfect drawing
int pixelWidth = (int)Math.Round(RenderSize.Width * DpiScaleX);
int pixelHeight = (int)Math.Round(RenderSize.Height * DpiScaleY);
// Exit if there is nothing to draw
if ((pixelWidth == 0) || (pixelHeight == 0)) return;
// Refresh the bitmap
Update(pixelWidth, pixelHeight);
if (bitmap != null)
// The bitmap will be oversized, and DrawingContext.DrawImage doesn't provide
// any other way to specify a source image rect
CroppedBitmap croppedBitmap = new CroppedBitmap(bitmap, new Int32Rect(0, 0, pixelWidth, pixelHeight));
dc.DrawImage(croppedBitmap, new Rect(default(Point), RenderSize));
在 XAML 中使用它:
<local:DitheredGradientImage TopLeftColor="#e5e5e5" TopRightColor="#ffffff" BottomLeftColor="#ffffff" BottomRightColor="#f5f5f5" />
【讨论】:
以上是关于如何让画笔平滑中间没有线条的主要内容,如果未能解决你的问题,请参考以下文章