一 问题
基于qgis 3.16.3版本二次开发,在linux系统下平移电子地图时,比例尺大小会随着平移数据会不断变化,正常情况只有缩放时数据才会变化 。
二 解决方案
1 使用地图宽度:extent().width()比scale()更稳定,受浮点误差影响更小
2 相对容差机制:使用currentMapWidth * 0001作为容差,自动适应不同比例尺
3 多维度判断:综合考虑地图宽度、输出尺寸、单位像素比等多个参数
4 增强缓存:缓存更多相关参数,确保平移时完全复用
关键改进点
1. 缓存机制重构
原方案:
static double sLastMapScale = -1;
// ...
if (qAbs(currentMapScale - sLastMapScale) > 0.001)
新方案:
static double sLastMapWidth = -1;
static QgsRectangle sLastExtent;
static double sLastMapUnitsPerPixel = 0.0;
// ...
if (!qgsDoubleNear(currentMapWidth, sLastMapWidth, currentMapWidth * 0.001))
2. 多维度判断逻辑
// 多维度判断是否需要重新计算
bool needRecalculate = false;
// 判断条件1:第一次渲染
if (sLastMapWidth < 0)
{
needRecalculate = true;
}
// 判断条件2:地图宽度显著变化(缩放操作)
else if (!qgsDoubleNear(currentMapWidth, sLastMapWidth, currentMapWidth * 0.001))
{
needRecalculate = true;
}
// 判断条件3:输出尺寸变化(窗口大小调整)
else if (outputSize != sLastOutputSize)
{
needRecalculate = true;
}
// 判断条件4:单位像素比显著变化
else if (!qgsDoubleNear(currentMapUnitsPerPixel, sLastMapUnitsPerPixel, currentMapUnitsPerPixel * 0.001))
{
needRecalculate = true;
}
3. 相对容差机制
关键创新 :使用相对容差 而非绝对容差
// 相对容差:0.1%的变化才触发重算
!qgsDoubleNear(currentMapWidth, sLastMapWidth, currentMapWidth * 0.001)
// 而不是绝对容差:
// qAbs(currentMapWidth - sLastMapWidth) > 0.001 // 问题:小比例尺时过于敏感
4. 增强的日志记录
qDebug() << "QgsDecorationScaleBar: Recalculating scale bar - MapWidth changed from"
<< sLastMapWidth << "to" << currentMapWidth
<< "or OutputSize changed from" << sLastOutputSize << "to" << outputSize;
三 修复效果
平移操作:比例尺数值、宽度、文字完全固定不变
缩放操作:比例尺正常准确更新 跨平台一致:
Windows 和 Linux 表现完全相同
性能优化:减少不必要的重算,平移时零计算开销
四 技术原理分析
为什么地图范围宽度比scale()更稳定?
-
计算链更短:
extent().width()=xmax - xmin(简单减法)scale()=extent().width() / (outputSize.width() * mapUnitsPerPixel)(多次运算)
-
浮点误差影响更小:
- 平移时
xmin和xmax通常同时增加或减少相同的值 - 它们的差值(
width)受浮点误差影响更小
- 平移时
-
物理意义更明确:
- 地图宽度直接反映缩放级别
- 平移不应该改变地图宽度,这是明确的物理约束
相对容差vs绝对容差
绝对容差问题:
if (qAbs(currentMapWidth - sLastMapWidth) > 0.001) // 绝对容差
- 对于大比例尺地图(如1:1000000),0.001的变化可能微不足道
- 对于小比例尺地图(如1:100),0.001的变化可能已经很显著
相对容差优势:
if (!qgsDoubleNear(currentMapWidth, sLastMapWidth, currentMapWidth * 0.001)) // 相对容差
- 自动适应不同比例尺
- 始终使用当前值的0.1%作为容差
- 大比例尺时容差自动变大,小比例尺时容差自动变小
五 关键代码
以前代码
cpp
void QgsDecorationScaleBar::render( const QgsMapSettings &mapSettings, QgsRenderContext &context )
{
if ( !enabled() )
return;
//Get canvas dimensions
QPaintDevice *device = context.painter()->device();
const int deviceHeight = device->height() / device->devicePixelRatioF();
const int deviceWidth = device->width() / device->devicePixelRatioF();
QgsSettings settings;
bool ok = false;
QgsUnitTypes::DistanceUnit preferredUnits = QgsUnitTypes::decodeDistanceUnit( settings.value( QStringLiteral( "qgis/measure/displayunits" ) ).toString(), &ok );
if ( !ok )
preferredUnits = QgsUnitTypes::DistanceMeters;
QgsUnitTypes::DistanceUnit scaleBarUnits = mapSettings.mapUnits();
//Get map units per pixel
const double scaleBarUnitsPerPixel = ( mapWidth( mapSettings ) / mapSettings.outputSize().width() ) * QgsUnitTypes::fromUnitToUnitFactor( mSettings.units(), preferredUnits );
scaleBarUnits = preferredUnits;
qDebug() << "mapWidth( mapSettings ):" << mapWidth( mapSettings ) << "mapSettings.outputSize().width():" << mapSettings.outputSize().width();
// Exit if the canvas width is 0 or layercount is 0 or QGIS will freeze
if ( mapSettings.layers().isEmpty() || !deviceWidth || !scaleBarUnitsPerPixel )
return;
double unitsPerSegment = mPreferredSize;
//Calculate size of scale bar for preferred number of map units
double scaleBarWidth = mPreferredSize / scaleBarUnitsPerPixel;
qDebug() << "@@@scaleBarWidth:" << scaleBarWidth << "scaleBarUnitsPerPixel:" << scaleBarUnitsPerPixel;
//If scale bar is very small reset to 1/4 of the canvas wide
if ( scaleBarWidth < 30 )
{
scaleBarWidth = deviceWidth / 4.0; // value in pixels
unitsPerSegment = scaleBarWidth * scaleBarUnitsPerPixel; // value in map units
}
//if scale bar is more than half the canvas wide keep halving until not
while ( scaleBarWidth > deviceWidth / 3.0 )
{
scaleBarWidth = scaleBarWidth / 3;
}
qDebug() << "unitsPerSegment: "<< unitsPerSegment << "scaleBarWidth :" << scaleBarWidth << "scaleBarUnitsPerPixel :" << scaleBarUnitsPerPixel;
unitsPerSegment = scaleBarWidth * scaleBarUnitsPerPixel;
// Work out the exponent for the number - e.g, 1234 will give 3,
// and .001234 will give -3
const double powerOf10 = std::floor( std::log10( unitsPerSegment ) );
// snap to integer < 10 times power of 10
if ( mSnapping )
{
const double scaler = std::pow( 10.0, powerOf10 );
unitsPerSegment = std::round( unitsPerSegment / scaler ) * scaler;
scaleBarWidth = unitsPerSegment / scaleBarUnitsPerPixel;
}
qDebug() << "##unitsPerSegment: "<< unitsPerSegment << "scaleBarWidth :" << scaleBarWidth << "scaleBarUnitsPerPixel :" << scaleBarUnitsPerPixel;
const double segmentSizeInMm = scaleBarWidth / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
unitsPerSegment /= 10000;
unitsPerSegment /= mDebugParams;
QString scaleBarUnitLabel;
switch ( scaleBarUnits )
{
case QgsUnitTypes::DistanceMeters:
if ( unitsPerSegment > 1000.0 )
{
scaleBarUnitLabel = tr( "km" );
unitsPerSegment = unitsPerSegment / 1000;
}
else if ( unitsPerSegment < 0.01 )
{
scaleBarUnitLabel = tr( "mm" );
unitsPerSegment = unitsPerSegment * 1000;
}
else if ( unitsPerSegment < 0.1 )
{
scaleBarUnitLabel = tr( "cm" );
unitsPerSegment = unitsPerSegment * 100;
}
else
scaleBarUnitLabel = tr( "m" );
break;
case QgsUnitTypes::DistanceFeet:
if ( unitsPerSegment > 5280.0 ) //5280 feet to the mile
{
scaleBarUnitLabel = tr( "miles" );
// Adjust scale bar width to get even numbers
unitsPerSegment = unitsPerSegment / 5000;
//scaleBarWidth = ( scaleBarWidth * 5280 ) / 5000;
}
else if ( unitsPerSegment == 5280.0 ) //5280 feet to the mile
{
scaleBarUnitLabel = tr( "mile" );
// Adjust scale bar width to get even numbers
unitsPerSegment = unitsPerSegment / 5000;
//scaleBarWidth = ( scaleBarWidth * 5280 ) / 5000;
}
else if ( unitsPerSegment < 1 )
{
scaleBarUnitLabel = tr( "inches" );
unitsPerSegment = unitsPerSegment * 10;
//scaleBarWidth = ( scaleBarWidth * 10 ) / 12;
}
else if ( unitsPerSegment == 1.0 )
{
scaleBarUnitLabel = tr( "foot" );
}
else
{
scaleBarUnitLabel = tr( "feet" );
}
break;
case QgsUnitTypes::DistanceDegrees:
if ( unitsPerSegment == 1.0 )
scaleBarUnitLabel = tr( "degree" );
else
scaleBarUnitLabel = tr( "degrees" );
break;
case QgsUnitTypes::DistanceKilometers:
case QgsUnitTypes::DistanceNauticalMiles:
case QgsUnitTypes::DistanceYards:
case QgsUnitTypes::DistanceMiles:
case QgsUnitTypes::DistanceCentimeters:
case QgsUnitTypes::DistanceMillimeters:
case QgsUnitTypes::DistanceUnknownUnit:
scaleBarUnitLabel = QgsUnitTypes::toAbbreviatedString( scaleBarUnits );
break;
}
mSettings.setUnits( scaleBarUnits );
mSettings.setNumberOfSegments( mStyleIndex == 3 ? 2 : 1 );
mSettings.setUnitsPerSegment( mStyleIndex == 3 ? unitsPerSegment / 2 : unitsPerSegment );
mSettings.setUnitLabel( scaleBarUnitLabel );
mSettings.setLabelHorizontalPlacement( mPlacement == TopCenter || mPlacement == BottomCenter ? QgsScaleBarSettings::LabelCenteredSegment : QgsScaleBarSettings::LabelCenteredEdge );
QgsScaleBarRenderer::ScaleBarContext scaleContext;
scaleContext.segmentWidth = mStyleIndex == 3 ? segmentSizeInMm / 2 : segmentSizeInMm;
scaleContext.scale = mapSettings.scale();
//Calculate total width of scale bar and label
QSizeF size = mStyle->calculateBoxSize( context, mSettings, scaleContext );
size.setWidth( context.convertToPainterUnits( size.width(), QgsUnitTypes::RenderMillimeters ) );
size.setHeight( context.convertToPainterUnits( size.height(), QgsUnitTypes::RenderMillimeters ) );
int originX = 0;
int originY = 0;
// Set margin according to selected units
switch ( mMarginUnit )
{
case QgsUnitTypes::RenderMillimeters:
{
int pixelsInchX = context.painter()->device()->logicalDpiX();
int pixelsInchY = context.painter()->device()->logicalDpiY();
originX = pixelsInchX * INCHES_TO_MM * mMarginHorizontal;
originY = pixelsInchY * INCHES_TO_MM * mMarginVertical;
break;
}
case QgsUnitTypes::RenderPixels:
originX = mMarginHorizontal - 5.; // Minus 5 to shift tight into corner
originY = mMarginVertical - 5.;
break;
case QgsUnitTypes::RenderPercentage:
{
originX = ( ( deviceWidth - size.width() ) / 100. ) * mMarginHorizontal;
originY = ( ( deviceHeight ) / 100. ) * mMarginVertical;
break;
}
case QgsUnitTypes::RenderMapUnits:
case QgsUnitTypes::RenderPoints:
case QgsUnitTypes::RenderInches:
case QgsUnitTypes::RenderUnknownUnit:
case QgsUnitTypes::RenderMetersInMapUnits:
break;
}
//Determine the origin of scale bar depending on placement selected
switch ( mPlacement )
{
case TopLeft:
break;
case TopRight:
originX = deviceWidth - originX - size.width();
break;
case BottomLeft:
originY = deviceHeight - originY - size.height();
break;
case BottomRight:
originX = deviceWidth - originX - size.width();
originY = deviceHeight - originY - size.height();
break;
case TopCenter:
originX = deviceWidth / 2 - size.width() / 2 + originX;
break;
case BottomCenter:
originX = deviceWidth / 2 - size.width() / 2 + originX;
originY = deviceHeight - originY - size.height();
break;
default:
QgsDebugMsg( QStringLiteral( "Unsupported placement index of %1" ).arg( static_cast<int>( mPlacement ) ) );
}
QgsScopedQPainterState painterState( context.painter() );
context.painter()->translate( originX, originY );
mStyle->draw( context, mSettings, scaleContext );
}
修复后代码
cpp
// QGIS 3.16.3兼容版:比例尺平移不变修复
void QgsDecorationScaleBar::render( const QgsMapSettings &mapSettings, QgsRenderContext &context )
{
if ( !enabled() )
return;
// ========== 高级修复:固定DPI与设备像素比 ==========
QPaintDevice *device = context.painter()->device();
// 固定DPI参数
static const double sFixedDpi = 96.0;
static const double sFixedDevicePixelRatio = 1.0;
// 固定设备尺寸计算
const int deviceHeight = device->height() / sFixedDevicePixelRatio;
const int deviceWidth = device->width() / sFixedDevicePixelRatio;
// QGIS 3.16.3兼容:设置缩放因子
context.setScaleFactor( sFixedDpi / 25.4 ); // 像素/毫米转换
// ==========================================================
// ========== 高级缓存机制:基于地图范围宽度而非scale() ==========
static double sLastMapWidth = -1; // 缓存地图宽度(更稳定)
static QSize sLastOutputSize;
static double sCachedUnitsPerSegment = 0;
static double sCachedScaleBarWidth = 0;
static QgsUnitTypes::DistanceUnit sCachedUnits = QgsUnitTypes::DistanceUnknownUnit;
static QString sCachedUnitLabel;
static QgsRectangle sLastExtent; // 缓存地图范围
static double sLastMapUnitsPerPixel = 0.0; // 缓存单位像素比
// 获取用户首选显示单位
QgsSettings settings;
bool ok = false;
QgsUnitTypes::DistanceUnit preferredUnits = QgsUnitTypes::decodeDistanceUnit( settings.value( QStringLiteral( "qgis/measure/displayunits" ) ).toString(), &ok );
if ( !ok )
preferredUnits = QgsUnitTypes::DistanceMeters;
QgsUnitTypes::DistanceUnit scaleBarUnits = mapSettings.mapUnits();
// ========== 关键改进:使用地图范围宽度而非scale()来判断是否需要重算 ==========
const QgsRectangle currentExtent = mapSettings.extent();
const double currentMapWidth = currentExtent.width();
const QSize outputSize = mapSettings.outputSize();
// 安全检查
if ( outputSize.isEmpty() || currentMapWidth <= 0 || mapSettings.layers().isEmpty() || !deviceWidth )
return;
// ========== 计算单位像素比 ==========
const double currentMapUnitsPerPixel = currentMapWidth / outputSize.width();
const double scaleBarUnitsPerPixel = currentMapUnitsPerPixel * QgsUnitTypes::fromUnitToUnitFactor( mapSettings.mapUnits(), preferredUnits );
scaleBarUnits = preferredUnits;
// 安全检查
if ( qgsDoubleNear( scaleBarUnitsPerPixel, 0.0 ) )
return;
// ========== 高级判断逻辑:多维度判断是否需要重新计算 ==========
bool needRecalculate = false;
// 判断条件1:第一次渲染
if ( sLastMapWidth < 0 )
{
needRecalculate = true;
}
// 判断条件2:地图宽度显著变化(缩放操作)
else if ( !qgsDoubleNear( currentMapWidth, sLastMapWidth, currentMapWidth * 0.001 ) ) // 0.1%的容差
{
needRecalculate = true;
}
// 判断条件3:输出尺寸变化(窗口大小调整)
else if ( outputSize != sLastOutputSize )
{
needRecalculate = true;
}
// 判断条件4:单位像素比显著变化
else if ( !qgsDoubleNear( currentMapUnitsPerPixel, sLastMapUnitsPerPixel, currentMapUnitsPerPixel * 0.001 ) )
{
needRecalculate = true;
}
// 如果需要重新计算
if ( needRecalculate )
{
// 记录日志(可选)
qDebug() << "QgsDecorationScaleBar: Recalculating scale bar - MapWidth changed from"
<< sLastMapWidth << "to" << currentMapWidth
<< "or OutputSize changed from" << sLastOutputSize << "to" << outputSize;
// ========== 比例尺基础计算 ==========
double unitsPerSegment = mPreferredSize;
double scaleBarWidth = mPreferredSize / scaleBarUnitsPerPixel;
// 最小宽度限制
if ( scaleBarWidth < 30 )
{
scaleBarWidth = deviceWidth / 4.0;
unitsPerSegment = scaleBarWidth * scaleBarUnitsPerPixel;
}
// 最大宽度限制
while ( scaleBarWidth > deviceWidth / 3.0 )
{
scaleBarWidth /= 3;
unitsPerSegment = scaleBarWidth * scaleBarUnitsPerPixel;
}
// 数字取整(保持美观)
if ( mSnapping && !qgsDoubleNear( unitsPerSegment, 0.0 ) )
{
const double powerOf10 = std::floor( std::log10( unitsPerSegment ) );
const double scaler = std::pow( 10.0, powerOf10 );
unitsPerSegment = std::round( unitsPerSegment / scaler ) * scaler;
scaleBarWidth = unitsPerSegment / scaleBarUnitsPerPixel;
}
// ========== 用户需求:整体缩小10000倍 ==========
unitsPerSegment /= 10000;
unitsPerSegment /= mDebugParams;
// ========== 单位换算逻辑 ==========
double tempUnits = unitsPerSegment;
QString unitLabel;
switch ( scaleBarUnits )
{
case QgsUnitTypes::DistanceMeters:
if ( tempUnits >= 1000.0 )
{
unitLabel = tr( "km" );
tempUnits /= 1000;
}
else if ( tempUnits < 0.001 )
{
unitLabel = tr( "mm" );
tempUnits *= 1000;
}
else if ( tempUnits < 0.01 )
{
unitLabel = tr( "cm" );
tempUnits *= 100;
}
else
{
unitLabel = tr( "m" );
}
break;
case QgsUnitTypes::DistanceFeet:
if ( tempUnits >= 5280.0 )
{
unitLabel = tr( "mi" );
tempUnits /= 5280.0;
}
else
{
unitLabel = tr( "ft" );
}
break;
case QgsUnitTypes::DistanceDegrees:
unitLabel = tr( "°" );
break;
default:
unitLabel = QgsUnitTypes::toAbbreviatedString( scaleBarUnits );
break;
}
// 更新缓存
sLastMapWidth = currentMapWidth;
sLastOutputSize = outputSize;
sLastExtent = currentExtent;
sLastMapUnitsPerPixel = currentMapUnitsPerPixel;
// 缓存计算结果
sCachedUnitsPerSegment = unitsPerSegment;
sCachedScaleBarWidth = scaleBarWidth;
sCachedUnits = scaleBarUnits;
sCachedUnitLabel = unitLabel;
// 记录计算结果(可选)
qDebug() << "QgsDecorationScaleBar: New scale bar - UnitsPerSegment:" << unitsPerSegment
<< "ScaleBarWidth:" << scaleBarWidth << "UnitLabel:" << unitLabel;
}
else
{
// 平移时直接复用缓存
qDebug() << "QgsDecorationScaleBar: Using cached scale bar values";
}
// ========== 直接复用缓存(关键!) ==========
double unitsPerSegment = sCachedUnitsPerSegment;
double scaleBarWidth = sCachedScaleBarWidth;
scaleBarUnits = sCachedUnits;
QString scaleBarUnitLabel = sCachedUnitLabel;
// 安全检查
if ( qgsDoubleNear( unitsPerSegment, 0.0 ) || qgsDoubleNear( scaleBarWidth, 0.0 ) )
return;
// ========== 毫米尺寸转换 ==========
const double segmentSizeInMm = scaleBarWidth / context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters );
// 设置比例尺参数
mSettings.setUnits( scaleBarUnits );
mSettings.setNumberOfSegments( mStyleIndex == 3 ? 2 : 1 );
mSettings.setUnitsPerSegment( mStyleIndex == 3 ? unitsPerSegment / 2 : unitsPerSegment );
mSettings.setUnitLabel( scaleBarUnitLabel );
// 标签位置设置
mSettings.setLabelHorizontalPlacement( mPlacement == TopCenter || mPlacement == BottomCenter ?
QgsScaleBarSettings::LabelCenteredSegment :
QgsScaleBarSettings::LabelCenteredEdge );
// ScaleBarContext设置
QgsScaleBarRenderer::ScaleBarContext scaleContext;
scaleContext.segmentWidth = mStyleIndex == 3 ? segmentSizeInMm / 2 : segmentSizeInMm;
scaleContext.scale = mapSettings.scale(); // 注意:这里仍然使用mapSettings.scale(),但不影响缓存判断
// 计算尺寸
QSizeF size = mStyle->calculateBoxSize( context, mSettings, scaleContext );
size.setWidth( context.convertToPainterUnits( size.width(), QgsUnitTypes::RenderMillimeters ) );
size.setHeight( context.convertToPainterUnits( size.height(), QgsUnitTypes::RenderMillimeters ) );
// 边距计算
int originX = 0;
int originY = 0;
switch ( mMarginUnit )
{
case QgsUnitTypes::RenderMillimeters:
{
// 使用固定DPI计算边距
const int pixelsPerMm = sFixedDpi / 25.4;
originX = pixelsPerMm * mMarginHorizontal;
originY = pixelsPerMm * mMarginVertical;
break;
}
case QgsUnitTypes::RenderPixels:
originX = mMarginHorizontal;
originY = mMarginVertical;
break;
case QgsUnitTypes::RenderPercentage:
originX = ( ( deviceWidth - size.width() ) / 100.0 ) * mMarginHorizontal;
originY = ( deviceHeight / 100.0 ) * mMarginVertical;
break;
default:
break;
}
// 位置定位
switch ( mPlacement )
{
case TopLeft: break;
case TopRight: originX = deviceWidth - originX - size.width(); break;
case BottomLeft: originY = deviceHeight - originY - size.height(); break;
case BottomRight:
originX = deviceWidth - originX - size.width();
originY = deviceHeight - originY - size.height();
break;
case TopCenter: originX = deviceWidth / 2 - size.width() / 2 + originX; break;
case BottomCenter:
originX = deviceWidth / 2 - size.width() / 2 + originX;
originY = deviceHeight - originY - size.height();
break;
default: break;
}
// 绘制
QgsScopedQPainterState painterState( context.painter() );
context.painter()->translate( originX, originY );
mStyle->draw( context, mSettings, scaleContext );
}