Monday, June 25, 2018

Change Width of DBText/AttributeReference by Its WidthFactor - Part One: Auto-Fitting

When a text entity (DBText) is added int drawing, very often there is only limited space available to place the text entity (mainly the space would limit the width of the text entity). In the case when text entity is created by AutoCAD user, if the text entity's width exceeds the allowed space, user can either change the text entity's text string by using different words/characters without losing the text string's meaning, or change the text entity's WidthFactor (assuming the text entity's style and height remain) so that the characters of the text string are rendered narrower than normal.

Changing text entity's overall width with its WidthFactor from Properties window is an easy and straightforward "try-and-see" process. However, in the case of attribute in a block (AttributeReference, which is derived from DBText, hence its overall widht can also be changed with its WidthFactor), it would be not that easy/straightforward to set its WidthFactor via Properties window.

Furthermore, as AutoCAD programmers, we often programmatically set text string of DBText or AttributeReference entities. In this case, we usually do not have the freedom to change the words/characters of the text string to reduce the overall width of the entities. Therefore, it would be natural to use code to automatically set WidthFactor to a proper value so that that the width of an DBText entity would fit a given space. For example, many drawing title blocks/borders have fields or tables to be filled with either DBTexts, or AttributeReferences programmatically. It would be good the application is able to adjust the width of DBText/AttributeReference to fit the text string in given space (width of the field or table column).

Assume I am in this situation: I need to use code to fill out a table with data from project database as text, or block's attribute. The text values could be too long to fit into the table's column width from time to time. So, as long as I know the width of the columns/fields, I want to shrink the text/attribute entity's width by change its WidthFactor automatically, so that user does not have to examine the table being updated by my application visually for the unfit texts.

The code to do the "auto-fit" work is really simple, shown below, as an extension method to ObjectId:

public static class TextWidthExtension
{
    public static bool FitTextWidth(
        this ObjectId entId, double width, double minWidthFactor=0.5)
    {
        var dfxName = entId.ObjectClass.DxfName.ToUpper();
        if (dfxName!="ATTRIB" && dfxName!="TEXT")
        {
            throw new ArgumentException(
                "Entity must be either DBText, or AttributeReference!");
        }
 
        bool fit = false;
 
        using (var tran = entId.Database.TransactionManager.StartTransaction())
        {
            var txtEnt = (DBText)tran.GetObject(entId, OpenMode.ForWrite);
            fit = FitToWidth(txtEnt, width, minWidthFactor);
            if (fit)
                tran.Commit();
            else
                tran.Abort();
        }
 
        return fit;
    }
 
    private static bool FitToWidth(DBText txt, double maxWidth, double minWidFactor)
    {
        var txtW = GetTextWidth(txt);
        bool fit = true;
        double factor = txt.WidthFactor;
 
        while (txtW > maxWidth && factor >= minWidFactor)
        {
            fit = false;
            factor -= 0.05;
            txt.WidthFactor = factor;
            txtW = GetTextWidth(txt);
        }
 
        fit = txtW < maxWidth;
 
        return fit;
    }
 
    private static double GetTextWidth(DBText txt)
    {
        var ext = txt.GeometricExtents;
        return Math.Abs(ext.MaxPoint.X - ext.MinPoint.X);
    }
}

This method takes an optional input to indicate the minimum value of WidthFactor of an DBText. In reality, it would make little sense to set WidthFactor value too small. Also, in the shown code, for simplicity, I calculate the width of a DBText entity with its GeometricExtents, assuming the DBText entity is horizontal (its Rotation=0.0). I could have also made the decrement of the WdithFactor in the "while{...}" loop a input parameter of the method instead of hard-coded as "0.05".

Here is the command to run the code:

using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(FitAttributeWidth.MyCommands))]
 
namespace FitAttributeWidth
{
    public class MyCommands
    {
        [CommandMethod("FitAtts")]
        public static void FitAttributeToSpace()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var attIds = GetBlockAttributeIds(ed);
                if (attIds.Count()>0)
                {
                    foreach (var id in attIds)
                    {
                        id.FitTextWidth(60.0, 0.7);
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError:\n{0}.", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        [CommandMethod("FitText")]
        public static void FitTextToSpace()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                while(true)
                {
                    var txtId = SelectText(ed);
                    if (!txtId.IsNull)
                    {
                        txtId.FitTextWidth(60.0, 0.7);
                    }
                    else
                    {
                        break;
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError:\n{0}.", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        #region private methods
 
        private static IEnumerable<ObjectId> GetBlockAttributeIds(Editor ed)
        {
            var lst = new List<ObjectId>();
 
            var opt = new PromptEntityOptions(
                "\nSelect the block for fitting its attributes:");
            opt.SetRejectMessage("\nInvalid selection: not a block.");
            opt.AddAllowedClass(typeof(BlockReference), true);
            var res = ed.GetEntity(opt);
            if (res.Status==PromptStatus.OK)
            {
                using (var tran = res.ObjectId.Database.TransactionManager.StartTransaction())
                {
                    var bref = (BlockReference)tran.GetObject(res.ObjectId, OpenMode.ForRead);
                    if (bref.AttributeCollection.Count>0)
                    {
                        lst.AddRange(bref.AttributeCollection.Cast<ObjectId>());
                    }
                    tran.Commit();
                }
            }
 
            return lst;
        }
 
        private static ObjectId SelectText(Editor ed)
        {
            ObjectId id = ObjectId.Null;
 
            var opt = new PromptEntityOptions(
                "\nSelect a Text entity in the table:");
            opt.SetRejectMessage("\nInvalid: not a DBText!");
            opt.AddAllowedClass(typeof(DBText), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                id = res.ObjectId;
            }
 
            return id;
        }
 
        #endregion
    }
}

This video clip shows the result of code execution.



3 comments:

  1. Yes, the code in this article should work with all versions of AutoCAD since AutoCAD 2013. Sorry, I did not keep the source code project somewhere. You have to create an AutoCAD .NET plugin project and copy the code from here. Hopefully, you do know how to build AutoCAD .NET plugin project with Visual Studio.

    ReplyDelete