使用JavaScript计算文本宽度

610

我想使用JavaScript来计算字符串的宽度,是否可以不使用等宽字体实现?

如果没有内置的方法,我的唯一想法是为每个字符创建一个宽度表,但这很不切实际,特别是支持Unicode和不同的字体大小(以及所有浏览器)。


15
请注意,如果您使用外部字体,则必须在它们加载后使用以下技术。如果您已经缓存它们或安装了本地版本,则可能没有意识到这一点。 - Seth W. Klein
29个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
599
在HTML 5中,您可以直接使用Canvas.measureText方法(更多解释在此处)。请尝试这个fiddle
/**
  * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
  * 
  * @param {String} text The text to be rendered.
  * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
  * 
  * @see https://dev59.com/EHVD5IYBdhLWcg3wAWsO#21015393
  */
function getTextWidth(text, font) {
  // re-use canvas object for better performance
  const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
  const context = canvas.getContext("2d");
  context.font = font;
  const metrics = context.measureText(text);
  return metrics.width;
}

function getCssStyle(element, prop) {
    return window.getComputedStyle(element, null).getPropertyValue(prop);
}

function getCanvasFont(el = document.body) {
  const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
  const fontSize = getCssStyle(el, 'font-size') || '16px';
  const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
  
  return `${fontWeight} ${fontSize} ${fontFamily}`;
}

console.log(getTextWidth("hello there!", "bold 12pt arial"));  // close to 86

如果您想使用特定元素myEl的字体大小,您可以使用getCanvasFont实用函数:

const fontSize = getTextWidth(text, getCanvasFont(myEl));
// do something with fontSize here...
解释:函数getCanvasFontSize获取某个元素(默认为)的字体并将其转换为与Context.font属性兼容的格式。 当然,任何元素在使用之前都必须先被添加到DOM中,否则它会给您提供虚假的值。 更多说明: 这种方法有几个优点,包括:
  • 比其他(基于DOM的)方法更加简洁和安全,因为它不会改变全局状态,如您的DOM。
  • 通过修改更多canvas文本属性,例如textAligntextBaseline,可以进行进一步的自定义。
注意:当您将文本添加到DOM时,请记得同时考虑padding、margin和border

注意2:在某些浏览器上,此方法可以提供亚像素精度(结果为浮点数),而在其他浏览器上则不行(结果仅为整数)。您可能需要对结果运行Math.floor(或Math.ceil)以避免不一致。由于基于DOM的方法永远不会达到亚像素精度,因此此方法甚至比这里介绍的其他方法具有更高的精度。

根据this jsperf(感谢评论中的贡献者),如果将缓存添加到基于DOM的方法中并且您没有使用Firefox,则Canvas方法基于DOM的方法的速度大约相同。在Firefox中,由于某种原因,此Canvas方法基于DOM的方法快得多(截至2014年9月)。

性能

这个示例将这个Canvas方法与Bob Monteverde的基于DOM的方法的变体进行比较,因此您可以分析和比较结果的准确性。


12
这是一个很棒的选项,我之前不知道。你有没有比较过这种方法与在DOM中进行测量的性能? - SimplGy
2
我在那个jsperf基准测试中添加了缓存。现在这两种方法基本上是相当的:http://jsperf.com/measure-text-width/4 - Brandon Bloom
1
@Martin 呃?这里和“许多HTML元素”有什么关系?在这个例子中,画布对象甚至没有附加到DOM上。即使是在这种情况下,更改画布绘图上下文中的字体直到你使用strokeText()fillText()才会影响画布。也许您想评论其他答案? - Ajedi32
1
@user1709076 您是在使用 Node 或其他不实际定义 DOM 的无头 JavaScript 环境吗?这样做是行不通的,除非您安装一个特殊的环境,比如 PhantomJS。但是在所有主流浏览器中都应该可以正常工作。请参见此处以查看支持的所有浏览器和版本的表格。 - Domi
2
好的,因为用户看不到它! - clabe45
显示剩余15条评论

414
创建一个DIV并使用以下样式进行修饰。在JavaScript中,设置你想要测量的字体大小和属性,将字符串放入DIV中,然后读取DIV的当前宽度和高度。它会自适应内容并且大小将与渲染出的字符串大小相差几个像素。

var fontSize = 12;
var test = document.getElementById("Test");
test.style.fontSize = fontSize;
var height = (test.clientHeight + 1) + "px";
var width = (test.clientWidth + 1) + "px"

console.log(height, width);
#Test
{
    position: absolute;
    visibility: hidden;
    height: auto;
    width: auto;
    white-space: nowrap; /* Thanks to Herb Caudill comment */
}
<div id="Test">
    abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
</div>


12
唯一需要补充的是,这可能会因使用的样式而给出错误的尺寸。请记住,您可能会使用像 p { letter-spacing: 0.1em; } 这样的样式,而 DIV 元素不会反映出来。您必须确保所使用的样式适用于文本所在的位置。 - Jim
7
同意Jim的评论 - 请仔细检查容器,即此处的div,是否应用了其他你可能没有注意到的css选择规则的样式。在测量之前,请将容器中的所有相关样式都清除,然后再应用你所关心的样式。 - Jason Bunting
55
如果您认为文本长度会超出浏览器宽度,您还应该添加“white-space:nowrap”,以保持文本不换行。 - Herb Caudill
5
@BBog 你所说的所有那些黑客操作,正是我建议采用不同、更少黑客手段的方法。在这里看一下它:https://dev59.com/EHVD5IYBdhLWcg3wAWsO#21015393。 - Domi
19
每个维度上的 +1 是用来偏移计算的。 - Kip
显示剩余7条评论

117

这是我没有示例就匆忙写出来的一个。看起来我们都在同一页上。

String.prototype.width = function(font) {
  var f = font || '12px arial',
      o = $('<div></div>')
            .text(this)
            .css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden', 'font': f})
            .appendTo($('body')),
      w = o.width();

  o.remove();

  return w;
}

使用它很简单:"a string".width()

**添加了white-space: nowrap,以便可以计算窗口宽度大于字符串宽度的字符串。


2
谢谢JordanC。我想补充一点,如果您在同一页上多次调用此函数,并且性能成为问题,您可以保留DIV,仅更改内容并检查宽度。我只是以这种方式完成一切,因此DOM与开始时相同。 - Bob Monteverde
5
我不会将这种方法添加到String中,因为它会破坏程序的“关注点分离”原则(即将核心代码和UI代码分开)。想象一下,如果你想将核心代码移植到一个使用非常不同的UI框架的平台上... - Domi
32
这是糟糕的设计,它不仅将您的模型与视图耦合,还直接与jQuery耦合。除此之外,这也是一个重排混乱,肯定不能很好地扩展。 - James
1
如果您想要浮点值,可以使用此处建议的 o[0].getBoundingClientRect().width:https://dev59.com/vHA65IYBdhLWcg3w1SbU#16072668 - Alexander Olsson
1
@virus 说得好,100多人可能也不关心性能... 尽管如此,这并不会使我所说的话变得无效或不真实。 - James
显示剩余3条评论

37

我喜欢你的“唯一思路”,即只做一个静态的字符宽度映射!对于我的目的来说,它实际上非常有效。有时候出于性能原因或者因为你无法轻松访问DOM,你可能只想要一个快速的、粗略的独立计算器来校准单个字体。所以这里有一个校准Helvetica的版本;传递一个字符串和字体大小:

const widths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.2796875,0.2765625,0.3546875,0.5546875,0.5546875,0.8890625,0.665625,0.190625,0.3328125,0.3328125,0.3890625,0.5828125,0.2765625,0.3328125,0.2765625,0.3015625,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.2765625,0.2765625,0.584375,0.5828125,0.584375,0.5546875,1.0140625,0.665625,0.665625,0.721875,0.721875,0.665625,0.609375,0.7765625,0.721875,0.2765625,0.5,0.665625,0.5546875,0.8328125,0.721875,0.7765625,0.665625,0.7765625,0.721875,0.665625,0.609375,0.721875,0.665625,0.94375,0.665625,0.665625,0.609375,0.2765625,0.3546875,0.2765625,0.4765625,0.5546875,0.3328125,0.5546875,0.5546875,0.5,0.5546875,0.5546875,0.2765625,0.5546875,0.5546875,0.221875,0.240625,0.5,0.221875,0.8328125,0.5546875,0.5546875,0.5546875,0.5546875,0.3328125,0.5,0.2765625,0.5546875,0.5,0.721875,0.5,0.5,0.5,0.3546875,0.259375,0.353125,0.5890625]
const avg = 0.5279276315789471

function measureText(str, fontSize) {
  return Array.from(str).reduce(
    (acc, cur) => acc + (widths[cur.charCodeAt(0)] ?? avg), 0
  ) * fontSize
}

那个巨大丑陋的数组是由字符代码索引的ASCII字符宽度。因此它只支持ASCII(否则会假定平均字符宽度)。幸运的是,宽度基本上与字体大小成线性比例关系,所以它在任何字体大小下都能很好地工作。它明显缺乏对字距或连字等的意识。

为了“校准”,我只需在svg上呈现每个字符直到charCode 126(强大的波浪号),然后获得边界框并将其保存到此数组中;更多代码、说明和演示请点击这里


1
这是一个聪明的解决方案,不错 :) 我总是使用相同的字体/大小,所以它很适合我。我发现DOM / Canvas解决方案的性能真的是个问题,这真的很干净,干杯! - mikeapr4
1
消除了对画布的需求 +1 - frage
4
注意,这并未考虑到每种现代字体都使用的字距对,以光学方式纠正特定字形之间的间距/跟踪。 - Jürg Lehni
2
@bryanph 当然可以,但在这种情况下,你的计算就简单了,只需 str.length * glyphWidth,因为等宽字体中的每个字形都具有相同的前进宽度。上面的解决方案适用于非等宽字体,而这些字体大多带有字距对,因此计算不会很精确。 - Jürg Lehni
给“强大的波浪号”点个赞 - temporary_user_name
显示剩余2条评论

35

这对我有效...

// Handy JavaScript to measure the size taken to render the supplied text;
// you can supply additional style information too if you have it.

function measureText(pText, pFontSize, pStyle) {
    var lDiv = document.createElement('div');

    document.body.appendChild(lDiv);

    if (pStyle != null) {
        lDiv.style = pStyle;
    }
    lDiv.style.fontSize = "" + pFontSize + "px";
    lDiv.style.position = "absolute";
    lDiv.style.left = -1000;
    lDiv.style.top = -1000;

    lDiv.textContent = pText;

    var lResult = {
        width: lDiv.clientWidth,
        height: lDiv.clientHeight
    };

    document.body.removeChild(lDiv);
    lDiv = null;

    return lResult;
}

你好,你的回答真的很有帮助。然而,为了支持IE 8及以下版本,请使用.cssText而不是.style。 - Dilip Rajkumar
这个答案很好,但是当我生成一大段文本时,它就会变得几个像素不准确。这是因为宽度不是整数吗? - Andrei
3
不要使用当前状态下的这个解决方案。使用textContent替代innerHTML以解决XSS漏洞。 - ethnanon
@Andrei 如果你想要更精确的结果,可以使用 getComputedStyle(lDiv).width - Octo Poulos

31

jQuery:

(function($) {

 $.textMetrics = function(el) {

  var h = 0, w = 0;

  var div = document.createElement('div');
  document.body.appendChild(div);
  $(div).css({
   position: 'absolute',
   left: -1000,
   top: -1000,
   display: 'none'
  });

  $(div).html($(el).html());
  var styles = ['font-size','font-style', 'font-weight', 'font-family','line-height', 'text-transform', 'letter-spacing'];
  $(styles).each(function() {
   var s = this.toString();
   $(div).css(s, $(el).css(s));
  });

  h = $(div).outerHeight();
  w = $(div).outerWidth();

  $(div).remove();

  var ret = {
   height: h,
   width: w
  };

  return ret;
 }

})(jQuery);

19

1
ExtJS有一个奇怪的许可证。你要么支付当前的维护者——Sencha公司来使用它,要么必须开源应用程序中所有相关的代码。这对大多数公司来说是一个停滞不前的问题。另一方面,jQuery使用高度宽松的MIT许可证。 - devdanke
1
JavaScript自动满足开源的要求吗?您可以向查看页面的任何人提供源代码。 - PatrickO
13
能够查看源代码和开源是有区别的,这取决于许可证的定义。 - Parker Ault
1
感谢那些仍在使用ExtJS的人们 :-) - Monarch Wadia
3
你应该让 ExtJS 安息。 - canbax

12

8
<span id="text">Text</span>

<script>
var textWidth = document.getElementById("text").offsetWidth;
</script>
只要标签没有其他样式应用于它,这个方法就可以工作。 offsetWidth将包括任何边框、水平填充、垂直滚动条宽度等的宽度。

1
这更或多或少是制作上述解决方案的方法,但由于您的文本可能会分成几行,他们会添加一些CSS样式到文本中以获得真正的完整文本宽度。 - Adrian Maire

8

您可以使用画布,这样您就不必处理太多的CSS属性:

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
ctx.font = "20pt Arial";  // This can be set programmaticly from the element's font-style if desired
var textWidth = ctx.measureText($("#myElement").text()).width;

这不是比创建一个DOM元素并为其指定CSS属性更加繁重(需要获取2D上下文等)吗? - Pharao2k
2
这是一种干净的方法,如果你已经在使用画布处理某些事情。但非常慢。仅 measureText 调用就需要大约 8-10 毫秒(在 Chrome 上)。 - Rui Marques

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,