Paths

Cocoa provides support for drawing simple or complex geometric shapes using paths. A path is a collection of points used to create primitive shapes such as lines, arcs, and curves. From these primitives, you can create more complex shapes, such as circles, rectangles, polygons, and complex curved shapes, and paint them. Because they are composed of points (as opposed to a rasterized bitmap), paths are lightweight, fast, and scale to different resolutions without losing precision or quality.

The following sections focus primarily on the use of the NSBezierPath class, which provides the main interface for creating and manipulating paths. Cocoa also provides a handful of functions that offer similar behavior for creating and drawing paths but do not require the overhead of creating an object. Those functions are mentioned where appropriate, but for more information, see Foundation Framework Reference and Application Kit Framework Reference.

Path Building Blocks

Cocoa defines several fundamental data types for manipulating geometric information in the drawing environment. These data types include NSPointNSRect, and NSSize. You use these data types to specify lines, rectangles, and width and height information for the shapes you want to draw. Everything from lines and rectangles to circles, arcs, and Bezier curves can be specified using one or more of these data structures.

The coordinate values for point, rectangle, and size data types are all specified using floating-point values. Floating-point values allow for much finer precision as the resolution of the underlying destination device goes up.

The NSPointNSRect, and NSSize data types have equivalents in the Quartz environment: CGPointCGRect, and CGSize. Because the layout of the Cocoa and Quartz types are identical, you can convert between two types by casting from one type to its counterpart.

The NSBezierPath Class


The NSBezierPath class provides the behavior for drawing most primitive shapes, and for many complex shapes, it is the only tool available in Cocoa. An NSBezierPathobject encapsulates the information associated with a path, including the points that define the path and the attributes that affect the appearance of the path. The following sections explain how NSBezierPath represents path information and also describe the attributes that affect a path’s appearance.

Path Elements

An NSBezierPath object uses path elements to build a path. A path element consists of a primitive command and one or more points. The command tells the path object how to interpret the associated points. When assembled, a set of path elements creates a series of line segments that form the desired shape.

The NSBezierPath class handles much of the work of creating and organizing path elements initially. Knowing how to manipulate path elements becomes important, however, if you want to make changes to an existing path. If you create a complex path based on user input, you might want to give the user the option of changing that path later. Although you could create a new path object with the changes, it is far simpler to modify the existing path elements. (For information on how to modify path elements, see“Manipulating Individual Path Elements.”)

The NSBezierPath class defines only four basic path element commands, which are listed in Table 5-1. These commands are enough to define all of the possible path shapes. Each command has one or more points that contain information needed to position the path element. Most path elements use the current drawing point as the starting point for drawing.

Table 5-1  Path element commands

Command

Number of points

Description

NSMoveToBezierPathElement

1

Moves the path object’s current drawing point to the specified point. This path element does not result in any drawing. Using this command in the middle of a path results in a disconnected line segment.

NSLineToBezierPathElement

1

Creates a straight line from the current drawing point to the specified point. Lines and rectangles are specified using thispath element.

NSCurveToBezierPathElement

3

Creates a curved line segment from the current point to the specified endpoint using two control points to define the curve. The points are stored in the following order: controlPoint1controlPoint2endPoint. Ovals, arcs, and Bezier curves all use curve elements to specify their geometry.

NSClosePathBezierPathElement

1

Marks the end of the current subpath at the specified point. (Note that the point specified for the Close Path element is essentially the same as the current point.

When you add a new shape to a path, NSBezierPath breaks that shape down into one or more component path elements for storage purposes. For example, callingmoveToPoint: or lineToPoint: creates a Move To element or Line To element respectively. In the case of more complex shapes, like rectangles and ovals, several line or curve elements may be created. Figure 5-1 shows two shapes and the resulting path elements. For the curved segment, the figure also shows the control points that define the curve.

Figure 5-1  Path elements for a complex path Path elements for a complex path

Listing 5-1 shows the code that creates the path shown in Figure 5-1.

Listing 5-1  Creating a complex path

NSBezierPath* aPath = [NSBezierPath bezierPath];
 
[aPath moveToPoint:NSMakePoint(0.0, 0.0)];
[aPath lineToPoint:NSMakePoint(10.0, 10.0)];
[aPath curveToPoint:NSMakePoint(18.0, 21.0)
        controlPoint1:NSMakePoint(6.0, 2.0)
        controlPoint2:NSMakePoint(28.0, 10.0)];
 
[aPath appendBezierPathWithRect:NSMakeRect(2.0, 16.0, 8.0, 5.0)];

Subpaths

subpath is a series of connected line and curve segments within an NSBezierPath object. A single path object may contain multiple subpaths, with each subpath delineated by a Move To or Close Path element. When you set the initial drawing point (typically using the moveToPoint: method), you set the starting point of the first subpath. As you draw, you build the contents of the subpath until you either close the path (using the closePath method) or add another Move To element. At that point, the subpath is considered closed and any new elements are added to a new subpath.

Some methods of NSBezierPath automatically create a new subpath for you. For example, creating a rectangle or oval results in the addition of a Move To element, several drawing elements, and a Close Path and Move To element (see Figure 5-1 for an example). The Move To element at the end of the list of elements ensures that the current drawing point is left in a known location, which in this case is at the rectangle’s origin point.

Subpaths exist to help you distinguish different parts of a path object. For example, subpaths affect the way a path is filled; see “Winding Rules.” The division of a path intosubpaths also affects methods such as bezierPathByReversingPath, which reverses the subpaths one at a time. In other cases, though, subpaths in an NSBezierPathobject share the same drawing attributes.

Path Attributes

An NSBezierPath object maintains all of the attributes needed to determine the shape of its path. These attributes include the line width, curve flatness, line cap style, line join style, and miter limit of the path. You set these values using the methods of NSBezierPath.

Path attributes do not take effect until you fill or stroke the path, so if you change an attribute more than once before drawing the path, only the last value is used. TheNSBezierPath class maintains both a custom and default version of each attribute. Path objects use custom attribute values if they are set. If no custom attribute value is set for a given path object, the default value is used. The NSBezierPath class does not use path attribute values set using Quartz functions.

Note: Path attributes apply to the entire path. If you want to use different attributes for different parts of a path, you must create two separate path objects and apply the appropriate attributes to each.

The following sections describe the attributes you can set for a path object and how those attributes affect your rendered paths.

Line Width

The line width attribute controls the width of the entire path. Line width is measured in points and specified as a floating-point value. The default width for all lines is 1. To change the default line width for all NSBezierPath objects, you use the setDefaultLineWidth: method. To set the line width for the current path object, you use thesetLineWidth: method of that path object. To set the default line width for shapes rendered without an NSBezierPath object, you must use the CGContextSetLineWidthfunction in Quartz.

Fractional line widths are rendered as close as possible to the specified width, subject to the limitations of the destination device, the position of the line, and the current anti-aliasing setting. For example, suppose you want to draw a line whose width is 0.2 points. Multiplying this width by 1/72 points per inch yields a line that is 0.0027778 inches wide. On a 90 dpi screen, the smallest possible line would be 1 pixel wide or 0.0111 inches. To ensure your line is not hidden on the screen, Cocoa nominally draws it at the screen’s larger minimum width (0.0111 inches). In reality, if the line straddles a pixel boundary or anti-aliasing is enabled, the line might affect additional pixels on either side of the path. If the output device were a 600 dpi printer instead, Quartz would be able to render the line closer to its true width of 0.0027778 inches.

Listing 5-2 draws a few paths using different techniques. The NSFrameRect function uses the default line width to draw a rectangle, so that value must be set prior to calling the function. Path objects use the default value only if a custom value has not been set. You can even change the line width of a path object and draw again to achieve a different path width, although you would also need to move the path to see the difference.

Listing 5-2  Setting the line width of a path

// Draw a rectangle using the default line width: 2.0.
[NSBezierPath setDefaultLineWidth:2.0];
NSFrameRect(NSMakeRect(20.0, 20.0, 10.0, 10.0));
 
// Set the line width for a single NSBezierPath object.
NSBezierPath* thePath = [NSBezierPath bezierPath];
[thePath setLineWidth:1.0]; // Has no effect.
[thePath moveToPoint:NSMakePoint(0.0, 0.0)];
[thePath lineToPoint:NSMakePoint(10.0, 0.0)];
[thePath setLineWidth:3.0];
[thePath lineToPoint:NSMakePoint(10.0, 10.0)];
 
// Because the last value set is 3.0, all lines are drawn with
// a width of 3.0, not just the second line.
[thePath stroke];
 
// Changing the width and stroking again draws the same path
// using the new line width.
[thePath setLineWidth:4.0];
[thePath stroke];
 
// Changing the default line width has no effect because a custom
// value already exists. The path is rendered with a width of 4.0.
[thePath setDefaultLineWidth:5.0];
[thePath stroke];
Line Cap Styles

The current line cap style determines the appearance of the open end points of a path segment. Cocoa supports the line cap styles shown in Figure 5-2.

Figure 5-2  Line cap styles Line cap styles

To set the line cap style for a NSBezierPath object, use the setLineCapStyle: method. The default line cap style is set to NSButtLineCapStyle. To change the default line cap style, use the setDefaultLineCapStyle: method. Listing 5-3 demonstrates both of these methods:

Listing 5-3  Setting the line cap style of a path

[// Set the default line cap style
[NSBezierPath setDefaultLineCapStyle:NSButtLineCapStyle];
 
// Customize the line cap style for the new object.
NSBezierPath* aPath = [NSBezierPath bezierPath];
[aPath moveToPoint:NSMakePoint(0.0, 0.0)];
[aPath lineToPoint:NSMakePoint(10.0, 10.0)];
[aPath setLineCapStyle:NSSquareLineCapStyle];
[aPath stroke];
Line Join Styles

The current line join style determines how connected lines in a path are joined at the vertices. Cocoa supports the line join styles shown in Figure 5-3.

Figure 5-3  Line join styles Line join styles

To set the line join style for an NSBezierPath object, use the setLineJoinStyle: method. The default line join style is set to NSMiterLineJoinStyle. To change the default line join style, use the setDefaultLineJoinStyle: method. Listing 5-4 demonstrates both of these methods:

Listing 5-4  Setting the line join style of a path

[// Set the default line join style
[NSBezierPath setDefaultLineJoinStyle:NSMiterLineJoinStyle];
 
// Customize the line join style for a new path.
NSBezierPath* aPath = [NSBezierPath bezierPath];
[aPath moveToPoint:NSMakePoint(0.0, 0.0)];
[aPath lineToPoint:NSMakePoint(10.0, 10.0)];
[aPath lineToPoint:NSMakePoint(10.0, 0.0)];
[aPath setLineJoinStyle:NSRoundLineJoinStyle];
[aPath stroke];
Line Dash Style

The line dash style determines the pattern used to stroke a path. By default, stroked paths appear solid. Using a line-dash pattern, you can specify an alternating group of solid and transparent swatches. When setting a line dash pattern, you specify the width (in points) of each successive solid or transparent swatch. The widths you specify are then repeated over the entire length of the path.

Figure 5-4 shows some sample line dash patterns, along with the values used to create each pattern.

Figure 5-4  Line dash patterns Line dash patterns

The NSBezierPath class does not support the concept of a default line dash style. If you want a line dash style, you must apply it to a path explicitly using thesetLineDash:count:phase: method as shown in Listing 5-5, which renders the last pattern from the preceding figure.

Listing 5-5  Adding a dash style to a path

void AddDashStyleToPath(NSBezierPath* thePath)
{
    // Set the line dash pattern.
    float lineDash[6];
 
    lineDash[0] = 40.0;
    lineDash[1] = 12.0;
    lineDash[2] = 8.0;
    lineDash[3] = 12.0;
    lineDash[4] = 8.0;
    lineDash[5] = 12.0;
 
   [thePath setLineDash:lineDash count:6 phase:0.0];
}
Line Flatness

The line flatness attribute determines the rendering accuracy for curved segments. The flatness value measures the maximum error tolerance (in pixels) to use during rendering. Smaller values result in smoother curves but require more computation time. Larger values result in more jagged curves but are rendered much faster.

Line flatness is one parameter you can tweak when you want to render a large number of curves quickly and do not care about accuracy. For example, you might increase this value during a live resize or scrolling operation when accuracy is not as crucial. Regardless, you should always measure performance to make sure such a modification actually saves time.

Figure 5-5 shows how changing the default flatness affects curved surfaces. The figure on the left shows a group of curved surfaces rendered with the flatness value set to 0.6(its default value). The figure on the right shows the same curved surfaces rendered with the flatness value set to 20. The curvature of each surface is lost and now appears to be a set of connected line segments.

Figure 5-5  Flatness effects on curves Flatness effects on curves

To set the flatness for a specific NSBezierPath object, use the setFlatness: method. To set the default flatness value, use setDefaultFlatness:, as shown in Listing 5-6:

Listing 5-6  Setting the flatness of a path

[- (void) drawRect:(NSRect)rect
{
    if ([self inLiveResize])
    {
        // Adjust the default flatness upward to reduce
        // the number of required computations.
        [NSBezierPath setDefaultFlatness:10.0];
 
        // Draw live resize content.
    }
    // ...
 
}
Miter Limits

Miter limits help you avoid spikes that occur when you join two line segments at a sharp angle. If the ratio of the miter length—the diagonal length of the miter—to the line thickness exceeds the miter limit, the corner is drawn using a bevel join instead of a miter join.

Note: Miter limits apply only to paths rendered using the miter join style.

Figure 5-6 shows an example of how different miter limits affect the same path. This path consists of several 10-point wide lines connected by miter joins. In the figure on the left, the miter limit is set to 5. Because the miter lengths exceed the miter limit, the line joins are changed to bevel joins. By increasing the miter limit to 16, as shown in the figure on the right, the miter joins are restored but extend far beyond the point where the two lines meet.

Figure 5-6  Miter limit effects Miter limit effects

To set the miter limits for a specific NSBezierPath object, use the setMiterLimit: method. To set the default miter limit for newly created NSBezierPath objects, usesetDefaultMiterLimit:Listing 5-7 demonstrates both of these methods:

Listing 5-7  Setting the miter limit for a path

// Increase the default limit
[NSBezierPath setDefaultMiterLimit:20.0];
 
// Customize the limit for a specific path with sharp angles.
NSBezierPath* aPath = [NSBezierPath bezierPath];
[aPath moveToPoint:NSMakePoint(0.0, 0.0)];
[aPath lineToPoint:NSMakePoint(8.0, 100.0)];
[aPath lineToPoint:NSMakePoint(16.0, 0.0)];
[aPath setLineWidth:5.0];
[aPath setMiterLimit:5.0];
[aPath stroke];

Winding Rules

When you fill the area encompassed by a path, NSBezierPath applies the current winding rule to determine which areas of the screen to fill. A winding rule is simply an algorithm that tracks information about each contiguous region that makes up the path's overall fill area. A ray is drawn from a point inside a given region to any point outside the path bounds. The total number of crossed path lines (including implicit lines) and the direction of each path line are then interpreted using the rules in Table 5-2, which determine if the region should be filled.

Table 5-2  Winding rules

Winding rule

Description

NSNonZeroWindingRule

Count each left-to-right path as +1 and each right-to-left path as -1. If the sum of all crossings is 0, the point is outside the path. If the sum isnonzero, the point is inside the path and the region containing it is filled. This is the default winding rule.

NSEvenOddWindingRule

Count the total number of path crossings. If the number of crossings is even, the point is outside the path. If the number of crossings is odd, the point is inside the path and the region containing it should be filled.

Fill operations are suitable for use with both open and closed subpaths. A closed subpath is a sequence of drawing calls that ends with a Close Path path element. An open subpath ends with a Move To path element. When you fill a partial subpath, NSBezierPath closes it for you automatically by creating an implicit (non-rendered) line from the first to the last point of the subpath.

Figure 5-7 shows how the winding rules are applied to a particular path. Subfigure a shows the path rendered using the nonzero rule and subfigure b shows it rendered using the even-odd rule. Subfigures c and d add direction marks and the hidden path line that closes the figure to help you see how the rules are applied to two of the path’s regions.

Figure 5-7  Applying winding rules to a path Applying winding rules to a path

To set the winding rule for an NSBezierPath object, use the setWindingRule: method. The default winding rule is NSNonZeroWindingRule. To change the default winding rule for all NSBezierPath objects, use the setDefaultWindingRule: method.

Manipulating Geometric Types

The Foundation framework includes numerous functions for manipulating geometric values and for performing various calculations using those values. In addition to basic equality checks, you can perform more complex operations, such as the union and intersection of rectangles or the inclusion of a point in a rectangle’s boundaries.

Table 5-3 lists some of the more commonly used functions and their behaviors. The function syntax is provided in a shorthand notation, with parameter types omitted to demonstrate the calling convention. For a complete list of available functions, and their full syntax, see the Functions section in Foundation Framework Reference.

Table 5-3  Commonly used geometry functions

Operation

Function

Description

Creation

NSPoint NSMakePoint(x, y)

Returns a properly formatted NSPoint data structure with the specified x and y values.

NSSize NSMakeSize(w, h)

Returns a properly formatted NSSize data structure with the specified width and height.

NSRect NSMakeRect(x, y, w, h)

Returns a properly formatted NSRect data structure with the specified origin (x, y) and size (width, height).

Equality

BOOL NSEqualPoints(p1, p2)

Returns YES if the two points are the same.

BOOL NSEqualSizes(s1, s2)

Returns YES if the two size types have identical widths and heights.

BOOL NSEqualRects(r1, r2)

Returns YES, if the two rectangles have the same origins and the same widths and heights.

Rectangle manipulations

BOOLNSContainsRect(r1, r2)

Returns YES if rectangle 1 completely encloses rectangle 2.

NSRect NSInsetRect(r, dX, dY)

Returns a copy of the specified rectangle with its sides moved inward by the specified delta values. Negative delta values move the sides outward. Does not modify the original rectangle.

NSRectNSIntersectionRect(r1, r2)

Returns the intersection of the two rectangles.

NSRect NSUnionRect(r1, r2)

Returns the union of the two rectangles.

BOOL NSMouseInRect(p, r, flipped)

Tests whether the point lies within the specified view rectangle. Adjusts the hit-detection algorithm to provide consistent behavior from the user’s perspective.

BOOL NSPointInRect(p, r)

Tests whether the point lies within the specified rectangle. This is a basic mathematical comparison.

Drawing Fundamental Shapes

For many types of content, path-based drawing has several advantages over image-based drawing:

  • Because paths are specified mathematically, they scale easily to different resolutions. Thus, the same path objects can be used for screen and print-based drawing.

  • The geometry information associated with a path requires much less storage space than most image data formats.

  • Rendering paths is often faster than compositing a comparable image. It takes less time to transfer path data to the graphics hardware than it takes to transfer the texture data associated with an image.

The following sections provide information about the primitive shapes you can draw using paths. You can combine one or more of these shapes to create a more complex path and then stroke or fill the path as described in “Drawing the Shapes in a Path.” For some shapes, there may be more than one way to add the shape to a path, or there may be alternate ways to draw the shape immediately. Wherever possible, the benefits and disadvantages of each technique are listed to help you decide which technique is most appropriate in specific situations.

Adding Points

An NSPoint structure by itself represents a location on the screen; it has no weight and cannot be drawn as such. To draw the equivalent of a point on the screen, you would need to create a small rectangle at the desired location, as shown in Listing 5-8.

Listing 5-8  Drawing a point

void DrawPoint(NSPoint aPoint)
{
    NSRect aRect = NSMakeRect(aPoint.x, aPoint.y, 1.0, 1.0);
 
    NSRectFill(aRect);
}

Of course, a more common use for points is to specify the position of other shapes. Many shapes require you to specify the current point before actually creating the shape. You set the current point using the moveToPoint: or relativeMoveToPoint: methods. Some shapes, like rectangles and ovals, already contain location information and do not require a separate call to moveToPoint:.

Important:  You must specify a starting point before drawing individual line, arc, curve, and glyph paths. If you do not, NSBezierPath raises an exception.

Adding Lines and Polygons

Cocoa provides a couple of options for adding lines to a path, with each technique offering different tradeoffs between efficiency and correctness. You can draw lines in the following ways:

  • Create single horizontal and vertical lines by filling a rectangle using NSRectFill. This technique is less precise but is often a little faster than creating an NSBezierPathobject. To create diagonal lines using this technique, you must apply a rotation transform before drawing. This technique is not appropriate for creating connected line segments.

  • Use the lineToPoint:relativeLineToPoint:, or strokeLineFromPoint:toPoint: methods of NSBezierPath to create individual or connected line segments. This technique is fast and is the most precise option for creating lines and complex polygons.

  • Use the appendBezierPathWithPoints:count: method to create a series of connected lines quickly. This technique is faster than adding individual lines.

Polygons are composed of multiple connected lines and should be created using an NSBezierPath object. The simplest way to create a four-sided nonrectangular shape, like a parallelogram, rhombus, or trapezoid, is using line segments. You could also create these shapes using transforms, but calculating the correct skew factors would require a lot more work.

Listing 5-9 shows code to draw a parallelogram using NSBezierPath. The method in this example inscribes the parallelogram inside the specified rectangle. The withShiftparameter specifies the horizontal shift applied to the top left and bottom right corners of the rectangular area.

Listing 5-9  Using lines to draw a polygon

void DrawParallelogramInRect(NSRect rect, float withShift)
{
    NSBezierPath* thePath = [NSBezierPath bezierPath];
 
    [thePath moveToPoint:rect.origin];
    [thePath lineToPoint:NSMakePoint(rect.origin.x - withShift,  rect.origin.y)];
    [thePath lineToPoint:NSMakePoint(NSMaxX(rect), NSMaxY(rect))];
    [thePath lineToPoint:NSMakePoint(rect.origin.x + withShift,  NSMaxY(rect))];
    [thePath closePath];
 
    [thePath stroke];
}

Adding Rectangles

Because rectangles are used frequently, there are several options for drawing them.

  • Use the methods of NSBezierPath to create your rectangle. The following methods are reasonably fast and offer the best precision:

  • Create rectangles using the Cocoa functions described in “Drawing Rectangles.” These functions draw rectangles faster than, but with less precision than, the methods ofNSBezierPath.

  • Create a rectangle using individual lines as described in “Adding Lines and Polygons.” You could use this technique to create diagonally oriented rectangles—that is, rectangles whose sides are not parallel to the x and y axes—without using a rotation transform.

Listing 5-10 shows a simple function that fills and strokes the same rectangle using two different techniques. The current fill and stroke colors are used when drawing the rectangle, along with the default compositing operation. In both cases, the rectangles are drawn immediately; there is no need to send a separate fill or stroke message.

Listing 5-10  Drawing a rectangle

void DrawRectangle(NSRect aRect)
{
    NSRectFill(aRect);
    [NSBezierPath strokeRect:aRect];
}

Adding Rounded Rectangles

In Mac OS X v10.5 and later, the NSBezierPath class includes the following methods for creating rounded-rectangles:

These methods create rectangles whose corners are curved according to the specified radius values. The radii describe the width and height of the oval to use at each corner of the rectangle. Figure 5-8 shows how this inscribed oval is used to define the path of the rectangle’s corner segments.

Figure 5-8  Inscribing the corner of a rounded rectangle Inscribing the corner of a rounded rectangle

Listing 5-11 shows a code snippet that creates and draws a path with a rounded rectangle.

Listing 5-11  Drawing a rounded rectangle

void DrawRoundedRect(NSRect rect, CGFloat x, CGFloat y)
{
    NSBezierPath* thePath = [NSBezierPath bezierPath];
 
    [thePath appendBezierPathWithRoundedRect:rect xRadius:x yRadius:y];
    [thePath stroke];
}

Adding Ovals and Circles

To draw ovals and circles, use the following methods of NSBezierPath:

Both methods inscribe an oval inside the rectangle you specify. You must then fill or stroke the path object to draw the oval in the current context. The following example creates an oval from the specified rectangle and strokes its path.

void DrawOvalInRect(NSRect ovalRect)
{
    NSBezierPath* thePath = [NSBezierPath bezierPath];
 
    [thePath appendBezierPathWithOvalInRect:ovalRect];
    [thePath stroke];
}

You could also create an oval using arcs, but doing so would duplicate what the preceding methods do internally and would be a little slower. The only reason to add individual arcs is to create a partial (non-closed) oval path. For more information, see “Adding Arcs.”

Adding Arcs

To draw arcs, use the following methods of NSBezierPath:

The appendBezierPathWithArcFromPoint:toPoint:radius: method creates arcs by inscribing them in an angle formed by the current point and the two points passed to the method. Inscribing a circle in this manner can result in an arc that does not intersect any of the points used to specify it. It can also result in the creation of an unwanted line from the current point to the starting point of the arc.

Figure 5-9 shows three different arcs and the control points used to create them. For the two arcs created using appendBezierPathWithArcFromPoint:toPoint:radius:, the current point must be set before calling the method. In both examples, the point is set to (30, 30). Because the radius of the second arc is shorter, and the starting point of the arc is not the same as the current point, a line is drawn from the current point to the starting point.

Figure 5-9  Creating arcs Creating arcs

Listing 5-12 shows the code snippets you would use to create each of the arcs from Figure 5-9. (Although the figure shows the arcs individually, executing the following code would render the arcs on top of each other. )

Listing 5-12  Creating three arcs

NSBezierPath*   arcPath1 = [NSBezierPath bezierPath];
NSBezierPath*   arcPath2 = [NSBezierPath bezierPath];
 
[[NSColor blackColor] setStroke];
 
// Create the first arc
[arcPath1 moveToPoint:NSMakePoint(30,30)];
[arcPath1 appendBezierPathWithArcFromPoint:NSMakePoint(0,30)  toPoint:NSMakePoint(0,60) radius:30];
[arcPath1 stroke];
 
// Create the second arc.
[arcPath2 moveToPoint:NSMakePoint(30,30)];
[arcPath2 appendBezierPathWithArcFromPoint:NSMakePoint(30,40)  toPoint:NSMakePoint(70,30) radius:20];
[arcPath2 stroke];
 
// Clear the old arc and do not set an initial point, which prevents a
// line being drawn from the current point to the start of the arc.
[arcPath2 removeAllPoints];
[arcPath2 appendBezierPathWithArcWithCenter:NSMakePoint(30,30) radius:30  startAngle:45 endAngle:135];
[arcPath2 stroke];

Adding Bezier Curves

To draw Bezier curves, you must use the curveToPoint:controlPoint1:controlPoint2: method of NSBezierPath. This method supports the creation of a cubic curve from the current point to the destination point you specify when calling the method. The controlPoint1 parameter determines the curvature starting from the current point, and controlPoint2 determines the curvature of the destination point, as shown in Figure 5-1.

Figure 5-10  Cubic Bezier curve Cubic Bezier curve

Adding Text

Because NSBezierPath only supports path-based content, you cannot add text characters directly to a path; instead, you must add glyphs. A glyph is the visual representation of a character (or partial character) in a particular font. For glyphs in an outline font, this visual representation is stored as a set of mathematical paths that can be added to anNSBezierPath object.

Note: Using NSBezierPath is not the most efficient way to render text, but can be used in situations where you need the path information associated with the text.

To obtain a set of glyphs, you can use the Cocoa text system or the NSFont class. Getting glyphs from the Cocoa text system is usually easier because you can get glyphs for an arbitrary string of characters, whereas using NSFont requires you to know the names of individual glyphs. To get glyphs from the Cocoa text system, you must do the following:

  1. Create the text system objects needed to manage text layout. For a tutorial on how to do this, see “Assembling the Text System by Hand” in Text System Overview.

  2. Use the glyphAtIndex: or getGlyphs:range: method of NSLayoutManager to retrieve the desired glyphs.

  3. Add the glyphs to your NSBezierPath object using one of the following methods:

When added to your NSBezierPath object, glyphs are converted to a series of path elements. These path elements simply specify lines and curves and do not retain any information about the characters themselves. You can manipulate paths containing glyphs just like you would any other path by changing the points of a path element or by modifying the path attributes.

Drawing the Shapes in a Path

There are two options for drawing the contents of a path: you can stroke the path or fill it. Stroking a path renders an outline of the path’s shape using the current stroke color and path attributes. Filling the path renders the area encompassed by the path using the current fill color and winding rule.

Figure 5-11 shows the same path from Figure 5-1 but with the contents filled and a different stroke width applied.

Figure 5-11  Stroking and filling a path. Stroking and filling a path.

Drawing Rectangles

Cocoa provides several functions for drawing rectangles to the current context immediately using the default attributes. These functions use Quartz primitives to draw one or more rectangles quickly, but in a way that may be less precise than if you were to use NSBezierPath. For example, these routines do not apply the current join style to the corners of a framed rectangle.

Table 5-4 lists some of the more commonly used functions for drawing rectangles along with their behaviors. You can use these functions in places where speed is more important than precision. The syntax for each function is provided in a shorthand notation, with parameter types omitted to demonstrate the calling conventions. For a complete list of available functions, and their full syntax, see Application Kit Functions Reference.

Table 5-4  Rectangle frame and fill functions

Function

Description

void NSEraseRect(aRect)

Fills the specified rectangle with white.

void NSFrameRect(aRect)

Draws the frame of the rectangle using the current fill color, the default line width, and the NSCompositeCopycompositing operation.

void NSFrameRectWithWidth(aRect, width)

Draws the frame of the rectangle using the current fill color, the specified width, and the NSCompositeCopycompositing operation.

voidNSFrameRectWithWidthUsingOperation(aRect, width, op)

Draws the frame of the rectangle using the current fill color, the specified width, and the specified operation.

void NSRectFill(aRect)

Fills the rectangle using the current fill color and the NSCompositeCopy compositing operation.

void NSRectFillUsingOperation(aRect, op)

Fills the rectangle using the current fill color and specified compositing operation.

void NSRectFillList(rectList, count)

Fills the C-style array of rectangles using the current fill color and the NSCompositeCopy compositing operation.

void NSRectFillListWithColors(rects, colors, count)

Fills the C-style array of rectangles using the corresponding list of colors. Each list must have the same number of entries.

void NSRectFillListUsingOperation(rects, count, op)

Fills the C-style array of rectangles using the current fill color and the specified compositing operation.

voidNSRectFillListWithColorsUsingOperation(rects, colors, count, op)

Fills the C-style array of rectangles using the corresponding list of colors and the specified compositing operation. The list of rectangles and list of colors must contain the same number of items.

Important: You may have noticed that the NSFrameRectNSFrameRectWithWidth, and NSFrameRectWithWidthUsingOperation functions draw the rectangle using the fill color instead of the stroke color. These methods draw the rectangle’s frame by filling four sub-rectangles, one for each side of the rectangle. This differs from the wayNSBezierPath draws rectangles and can sometimes lead to confusion. If your rectangle does not show up the way you expected, check your code to make sure you are setting the drawing color using either the set or setFill method of NSColor.

Working with Paths

Building a sleek and attractive user interface is hard work and most programs use a combination of images and paths to do it. Paths have the advantage of being lightweight, scalable, and fast. Even so, paths are not appropriate in all situations. The following sections provide some basic tips and guidance on how to use paths effectively in your program.

Building Paths

Building a path involves creating an NSBezierPath object and adding path elements to it. All paths must start with a Move To element to mark the first point of the path. In some cases, this element is added for you but in others you must add it yourself. For example, methods that create a closed path (such as an oval or rectangle) insert a MoveTo element for you.

A single NSBezierPath object may have multiple subpaths. Each subpath is itself a complete path, meaning the subpath may not appear connected to any other subpaths when drawn. Filled subpaths can still interact with each other, however. Overlapping subpaths may cancel each other’s fill effect, resulting in holes in the fill area.

All subpaths in an NSBezierPath object share the same drawing attributes. The only way to assign different attributes to different paths is to create different NSBezierPathobjects for each.

Improving Rendering Performance

As you work on your drawing code, you should keep performance in mind. Drawing is a processor intensive activity but there are many ways to reduce the amount of drawing performed by your application. The following sections offer some basic tips related to improving drawing performance with Cocoa applications. For additional drawing-related performance tips, see Drawing Performance Guidelines.

Note: As with any determination of performance, you should measure the speed of your drawing operations before making any changes. If the amount of time spent inside the methods of NSBezierPath becomes significant, simplifying your paths might offer better performance. Limiting the total amount of drawing you do during an update cycle might also improve performance.

Reuse Your Path Objects

If you draw the same content repeatedly, consider caching the objects used to draw that content. It is usually more efficient to retain an existing NSBezierPath object than to recreate it during each drawing cycle. For content that might change dynamically, you might also consider maintaining a pool of reusable objects.

Correctness Versus Efficiency

When writing your drawing code, you should always try to make that code as efficient as possible without sacrificing the quality of the rendered content. If your drawing code seems slow, there are some tradeoffs you can make to improve efficiency that reduce quality only temporarily:

  • Use the available update rectangles to draw only what has changed. Use different NSBezierPath objects for each part of the screen rather than one large object that covers everything. For more information, see “Reduce Path Complexity.”

  • During scrolling, live resizing, or other time-critical operations, consider the following options:

    • If your screen contains animated content, pause the animation until the operation is complete.

    • Try temporarily increasing the flatness value for curved paths. The default flatness value is set to 0.6, which results in nice smooth curves. Increasing this value above 1.0 may make your curves look more jagged but should improve performance. You may want to try a few different values to determine a good tradeoff between appearance and speed.

    • Disable anti-aliasing. For more information, see “Setting the Anti-aliasing Options.”

  • When drawing rectangles, use NSFrameRect and NSRectFill for operations where the highest quality is not required. These functions offer close approximations to what you would get with NSBezierPath but are often a little faster.

Reduce Path Complexity

If you are drawing a large amount of content, you should do your best to reduce the complexity of the path data you store in a single NSBezierPath object. Path objects with hundreds of path elements require more calculations than those with 10 or 20 elements. Every line or curve segment you add increases the number of calculations required to flatten the path or determine whether a point is inside it. Numerous path crossings also increases the number of required calculations when filling the path.

If the accuracy of rendered paths is not crucial, try using multiple NSBezierPath objects to draw the same content. There is very little visual difference between using one path object or multiple path objects. If your path is already grouped into multiple subpaths, then it becomes easy to put some of those subpaths in other NSBezierPath objects. Using multiple path objects reduces the number of calculations for each subpath and also allows you to limit rendering to only those paths that are in the current updaterectangle.

Manipulating Individual Path Elements

Given an NSBezierPath object with some existing path data, you can retrieve the points associated with that path and modify them individually. An illustration program might do this in response to a mouse event over one of the points in a path. If the mouse event results in that point being dragged to a new location, you can quickly update the path element with the new location and redraw the path.

The elementCount method of NSBezierPath returns the total number of path elements for all subpaths of the object. To find out the type of a given path element, use theelementAtIndex: or elementAtIndex:associatedPoints: method. These methods return one of the values listed in Table 5-1. Use theelementAtIndex:associatedPoints: method if you also want to retrieve the points associated with an element. If you do not already know the type of the path element, you should pass this method an array capable of holding at least three NSPoint data types.

To change the points associated with a path element, use the setAssociatedPoints:atIndex: method. You cannot change the type of a path element, only the points associated with it. When changing the points, NSBezierPath takes only as many points from your point array as are needed. For example, if you specify three points for a Line To path element, only the first point is used.

Listing 5-13 shows a method that updates the control point associated with a curve path element on the end of the current path. The points that define the curve are stored in the order controlPoint1controlPoint2endPoint. This method replaces the point controlPoint2, which affects the end portion of the curve.

Listing 5-13  Changing the control point of a curve path element

- (void)replaceLastControlPointWithPoint:(NSPoint)newControl
            inPath:(NSBezierPath*)thePath
{
    int elemCount = [thePath elementCount];
    NSBezierPathElement elemType =
                [thePath elementAtIndex:(elemCount - 1)];
 
    if (elemType != NSCurveToBezierPathElement)
        return;
 
    // Get the current points for the curve.
    NSPoint points[3];
    [thePath elementAtIndex:(elemCount - 1) associatedPoints:points];
 
    // Replace the old control point.
    points[1] = newControl;
 
    // Update the points.
    [thePath setAssociatedPoints:points atIndex:(elemCount - 1)];
}

Transforming a Path

The coordinate system of an NSBezierPath object always matches the coordinate system of the view in which it is drawn. Thus, given a path whose first point is at (0, 0) in your NSBezierPath object, drawing the path in your view places that point at (0, 0) in the view’s current coordinate system. To draw that path in a different location, you must apply a transform in one of two ways:

Both techniques cause the path to be drawn at the same location in the view; however, the second technique also has the side effect of permanently modifying theNSBezierPath object. Depending on your content, this may or may not be appropriate. For example, in an illustration program, you might want the user to be able to drag shapes around the view; therefore, you would want to modify the NSBezierPath object to retain the new position of the path.

Creating a CGPathRef From an NSBezierPath Object

There may be times when it is necessary to convert an NSBezierPath object to a CGPathRef data type so that you can perform path-based operations using Quartz. For example, you might want to draw your path to a Quartz transparency layer or use it to do advanced hit detection. Although you cannot use a NSBezierPath object directly from Quartz, you can use its path elements to build a CGPathRef object.

Listing 5-14 shows you how to create a CGPathRef data type from an NSBezierPath object. The example extends the behavior of the NSBezierPath class using a category. The quartzPath method uses the path elements of the NSBezierPath object to call the appropriate Quartz path creation functions. Although the method creates a mutableCGPathRef object, it returns an immutable copy for drawing. To ensure that the returned path returns correct results during hit detection, this method implicitly closes the last subpath if your code does not do so explicitly. Quartz requires paths to be closed in order to do hit detection on the path’s fill area.

Listing 5-14  Creating a CGPathRef from an NSBezierPath

@implementation NSBezierPath (BezierPathQuartzUtilities)
// This method works only in Mac OS X v10.2 and later.
- (CGPathRef)quartzPath
{
    int i, numElements;
 
    // Need to begin a path here.
    CGPathRef           immutablePath = NULL;
 
    // Then draw the path elements.
    numElements = [self elementCount];
    if (numElements > 0)
    {
        CGMutablePathRef    path = CGPathCreateMutable();
        NSPoint             points[3];
        BOOL                didClosePath = YES;
 
        for (i = 0; i < numElements; i++)
        {
            switch ([self elementAtIndex:i associatedPoints:points])
            {
                case NSMoveToBezierPathElement:
                    CGPathMoveToPoint(path, NULL, points[0].x, points[0].y);
                    break;
 
                case NSLineToBezierPathElement:
                    CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y);
                    didClosePath = NO;
                    break;
 
                case NSCurveToBezierPathElement:
                    CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y,
                                        points[1].x, points[1].y,
                                        points[2].x, points[2].y);
                    didClosePath = NO;
                    break;
 
                case NSClosePathBezierPathElement:
                    CGPathCloseSubpath(path);
                    didClosePath = YES;
                    break;
            }
        }
 
        // Be sure the path is closed or Quartz may not do valid hit detection.
        if (!didClosePath)
            CGPathCloseSubpath(path);
 
        immutablePath = CGPathCreateCopy(path);
        CGPathRelease(path);
    }
 
    return immutablePath;
}
@end

The code from the preceding example closes only the last open path by default. Depending on your path objects, you might also want to close intermediate subpaths whenever a new Move To element is encountered. If your path objects typically contain only one path, you do not need to do so, however.

Detecting Mouse Hits on a Path

If you need to determine whether a mouse event occurred on a path or its fill area, you can use the containsPoint: method of NSBezierPath. This method tests the point against all closed and open subpaths in the path object. If the point lies on or inside any of the subpaths, the method returns YES. When determining whether a point is inside a subpath, the method uses the nonzero winding rule.

If your software runs in Mac OS X v10.4 and later, you can perform more advanced hit detection using the CGContextPathContainsPoint and CGPathContainsPointfunctions in Quartz. Using these functions you can determine if a point is on the path itself or if the point is inside the path using either the nonzero or even-odd winding rule. Although you cannot use these functions on an NSBezierPath object directly, you can convert your path object to a CGPathRef data type and then use them. For information on how to convert a path object to a CGPathRef data type, see “Creating a CGPathRef From an NSBezierPath Object.”

Important: Quartz considers a point to be inside a path only if the path is explicitly closed. If you are converting your NSBezierPath objects to Quartz paths for use in hit detection, be sure to close any open subpaths either prior to or during the conversion. If you do not, points lying inside your path may not be correctly identified as such.

Listing 5-15 shows an example of how you might perform advanced hit detection on an NSBezierPath object. This example adds a method to the NSBezierPath class using a category. The implementation of the method adds a CGPathRef version of the current path to the current context and calls the CGContextPathContainsPoint function. This function uses the specified mode to analyze the location of the specified point relative to the current path and returns an appropriate value. Modes can includekCGPathFillkCGPathEOFillkCGPathStrokekCGPathFillStroke, or kCGPathEOFillStroke.

Listing 5-15  Detecting hits on a path

@implementation NSBezierPath (BezierPathQuartzUtilities)
// Note, this method works only in Mac OS X v10.4 and later.
- (BOOL)pathContainsPoint:(NSPoint)point forMode:(CGPathDrawingMode)mode
{
    CGPathRef       path = [self quartzPath]; // Custom method to create a CGPath
    CGContextRef    cgContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
    CGPoint         cgPoint;
    BOOL            containsPoint = NO;
 
    cgPoint.x = point.x;
    cgPoint.y = point.y;
 
    // Save the graphics state before doing the hit detection.
    CGContextSaveGState(cgContext);
 
    CGContextAddPath(cgContext, path);
    containsPoint = CGContextPathContainsPoint(cgContext, cgPoint, mode);
 
    CGContextRestoreGState(cgContext);
 
    return containsPoint;
}
@end
<template> <BasicModal v-bind="$attrs" @register="registerModal" :title="title" destroyOnClose width="60%" @ok="handleSubmit" @visible-change="visibleChange"> <BasicForm @register="registerForm"> <template #title="{ model, field }"> <div class="item"> <div class="title-view"> <div class="tag"></div> <div class="title">{{ model[field] }}</div> </div> </div> </template> <template #getTreeBtn> <a-button type="primary" @click="createTree">获取目录树</a-button> </template> <template #getTree> <div class="item"> <a-card :bordered="false" style="height: 100%"> <a-spin :spinning="loading"> <template v-if="treeData.length > 0"> <a-tree v-if="!treeReloading" checkable :clickRowToExpand="true" :treeData="treeData" :selectedKeys="selectedKeys" :checkStrictly="true" :load-data="loadChildrenTreeData" :checkedKeys="checkedKeysRef" v-model:expandedKeys="expandedKeys" @check="onCheck" @select="onSelect" @expand="onExpand" > <template #title="{ key: treeKey, title }"> <span>{{ title }}</span> </template> </a-tree> </template> <a-empty v-else description="暂无数据" /> </a-spin> </a-card> </div> </template> <template #ziduan> <editTable :taskType="2" :data="dataSource" @save="saveData" /> </template> </BasicForm> </BasicModal> </template> <script setup lang="ts"> import { ref, onMounted, defineEmits } from 'vue'; import { formSchema } from '../user.data'; import { BasicForm, useForm } from '/@/components/Form/index'; import { message } from 'ant-design-vue'; import { useUserStore } from '/@/store/modules/user'; import { dirList, saveOrUpdate } from '../user.api'; import { BasicModal, useModalInner } from '/@/components/Modal'; import editTable from '../../database/components/editTable.vue'; // 状态定义 const treeData = ref<any[]>([]); const checkedKeysRef = ref<string[]>([]); const selectedKeys = ref<string[]>([]); const expandedKeys = ref<string[]>([]); const loading = ref<boolean>(false); const treeReloading = ref<boolean>(false); const paths = ref<string[]>([]); const dataSource = ref(); const fieldInfoObj = ref(); const isUpdate = ref(false); const title = ref(); const editDeviceName = ref(); const emit = defineEmits(['success']); const deviceTypeOptions = ref(); // 初始化 onMounted(async () => { deviceTypeOptions.value = useUserStore().getAllDictItems['dc_device_type'] || []; }); // 表单配置 const [registerForm, { resetFields, setFieldsValue, validate, getFieldsValue }] = useForm({ labelWidth: 100, schemas: formSchema, showActionButtonGroup: false, baseColProps: { span: 12 }, }); // 递归查找 treeData 中匹配 path 的 key function findKeysByPaths(treeNodes: any[], pathsToMatch: string[]) { const keys: string[] = []; treeNodes.forEach((node) => { if (pathsToMatch.includes(node.path)) { keys.push(node.key); } if (node.children && node.children.length > 0) { keys.push(...findKeysByPaths(node.children, pathsToMatch)); } }); return keys; } // 查找 treeData 中匹配 path 的节点 function findNodeByPath(treeNodes: any[], path: string) { for (const node of treeNodes) { if (node.path === path) { return node; } if (node.children && node.children.length > 0) { const found = findNodeByPath(node.children, path); if (found) return found; } } return null; } // 递归加载路径上的所有节点 async function loadPathNodes(path: string, parentNode: any = null) { console.log('loadPathNodes: Processing path:', path); const segments = path.split('/').filter((seg) => seg); let currentPath = ''; let currentNodes = parentNode ? parentNode.children : treeData.value; let currentParent = parentNode; for (let i = 0; i < segments.length; i++) { currentPath = i === 0 ? `/${segments[0]}` : `${currentPath}/${segments[i]}`; let node = findNodeByPath(currentNodes, currentPath); console.log('loadPathNodes: Current path:', currentPath, 'Node found:', !!node); if (!node && currentParent && currentParent.izLeaf) { console.log('loadPathNodes: Loading children for:', currentParent.path); await loadChildrenTreeData({ dataRef: currentParent }); currentNodes = currentParent.children || []; node = findNodeByPath(currentNodes, currentPath); } if (node) { expandedKeys.value = [...new Set([...expandedKeys.value, node.key])]; if (node.izLeaf && i < segments.length - 1) { console.log('loadPathNodes: Preloading children for:', node.path); await loadChildrenTreeData({ dataRef: node }); } currentParent = node; currentNodes = node.children || []; } else { console.warn('loadPathNodes: Node not found for path:', currentPath); break; } } treeData.value = [...treeData.value]; // 强制更新 treeData console.log('loadPathNodes: Updated treeData:', treeData.value); } // 解析并加载所有路径的父节点 async function expandParentNodes(pathsToExpand: string[]) { console.log('expandParentNodes: Processing paths:', pathsToExpand); expandedKeys.value = []; for (const path of pathsToExpand) { await loadPathNodes(path); } console.log('expandParentNodes: Final expandedKeys:', expandedKeys.value); treeData.value = [...treeData.value]; // 确保响应式更新 } // 模态框初始化 const [registerModal, { closeModal }] = useModalInner(async (data) => { await resetFields(); isUpdate.value = data.isUpdate; if (data.isUpdate) { editDeviceName.value = data.record.deviceName; paths.value = data.record.configInfoObj.paths || []; fieldInfoObj.value = dataSource.value = data.record.fieldInfoObj || []; title.value = '修改任务'; await setFieldsValue({ ...data.record, ...data.record.configInfoObj, }); await new Promise((resolve) => setTimeout(resolve, 10)); await setFieldsValue({ deviceId: data.record.deviceId }); console.log('Modal init: paths:', paths.value); await handleTree(); if (paths.value.length > 0) { await expandParentNodes(paths.value); checkedKeysRef.value = findKeysByPaths(treeData.value, paths.value); console.log('Modal init: Backfill - paths:', paths.value, 'checkedKeys:', checkedKeysRef.value); } } else { fieldInfoObj.value = dataSource.value = []; paths.value = []; checkedKeysRef.value = []; title.value = '创建任务'; await setFieldsValue({ ...data.record, }); } }); // 将数据格式化为树结构 function formatTreeData(item: any, index: number) { return { title: item.name, key: `${item.path}-${item.name}-${index}`, izLeaf: item.izLeaf, isLeaf: !item.izLeaf, path: item.path, parentPath: item.parentPath, children: [], // 初始化 children 为空数组 }; } // 获取目录树 async function handleTree() { console.log('handleTree: Starting, current paths:', paths.value); const savedPaths = [...paths.value]; // 保存当前路径 const savedExpandedKeys = [...expandedKeys.value]; // 保存当前展开状态 let values = await getFieldsValue(); let params = { host: values.host, port: values.port, username: values.username, password: values.password, os: values.os, path: '/', }; console.log('handleTree: dirList params:', params); if (values.host && values.port && values.username && values.password && values.os) { try { loading.value = true; let data = await dirList({ ...params }); console.log('handleTree: dirList result:', data); if (!data || data.length === 0) { message.error('目录树数据为空,请检查FTP服务器配置'); treeData.value = []; paths.value = []; checkedKeysRef.value = []; expandedKeys.value = []; return; } treeData.value = data.map((item, index) => formatTreeData(item, index)); console.log('handleTree: Initial treeData:', treeData.value); autoExpandParentNode(); if (isUpdate.value && savedPaths.length > 0) { paths.value = savedPaths; // 恢复路径 console.log('handleTree: Restoring paths:', paths.value); await expandParentNodes(paths.value); // 重新加载深层路径 checkedKeysRef.value = findKeysByPaths(treeData.value, paths.value); expandedKeys.value = [...new Set([...expandedKeys.value, ...savedExpandedKeys])]; // 恢复展开状态 console.log('handleTree: After restore - paths:', paths.value, 'checkedKeys:', checkedKeysRef.value, 'expandedKeys:', expandedKeys.value); } } catch (error) { console.error('handleTree: Error:', error); message.error('获取目录树失败:' + error.message); treeData.value = []; paths.value = []; checkedKeysRef.value = []; expandedKeys.value = []; } finally { loading.value = false; } } else { message.info('请填写完整FTP服务器配置再获取目录树'); } } // 自动展开父节点 function autoExpandParentNode() { let item = treeData.value[0]; if (item && item.izLeaf) { expandedKeys.value = [...new Set([...expandedKeys.value, item.key])]; } console.log('autoExpandParentNode: expandedKeys:', expandedKeys.value); reloadTree(); } // 重新加载树 async function reloadTree() { treeReloading.value = true; await new Promise((resolve) => setTimeout(resolve, 0)); treeReloading.value = false; } // 动态加载子节点 async function loadChildrenTreeData(treeNode: any) { try { console.log('loadChildrenTreeData: Triggered for path:', treeNode.dataRef.path); if (treeNode.dataRef.izLeaf) { let values = await getFieldsValue(); let params = { host: values.host, port: values.port, username: values.username, password: values.password, os: values.os, path: treeNode.dataRef.path, }; console.log('loadChildrenTreeData: dirList params:', params); const result = await dirList(params); console.log('loadChildrenTreeData: dirList result:', result); if (result.length === 0) { treeNode.dataRef.izLeaf = false; treeNode.dataRef.isLeaf = true; } else { treeNode.dataRef.children = result.map((item, index) => formatTreeData(item, index)); treeNode.dataRef.isLeaf = false; // 确保有子节点时不标记为叶子节点 } treeData.value = [...treeData.value]; // 深拷贝触发响应式更新 if (isUpdate.value && paths.value.length > 0) { checkedKeysRef.value = findKeysByPaths(treeData.value, paths.value); console.log('loadChildrenTreeData: After load - checkedKeys:', checkedKeysRef.value); } } else { console.log('loadChildrenTreeData: Node is not a folder, izLeaf:', treeNode.dataRef.izLeaf); } } catch (e) { console.error('loadChildrenTreeData: Error:', e); message.error('加载子节点失败:' + e.message); } return Promise.resolve(); } // 展开事件 async function onExpand(expandedKeys: string[], { expanded, node }: { expanded: boolean; node: any }) { expandedKeys.value = expandedKeys; console.log('onExpand: Node:', node.path, 'Expanded:', expanded, 'izLeaf:', node.izLeaf); if (expanded && node.izLeaf && (!node.children || !node.children.length)) { await loadChildrenTreeData({ dataRef: node }); if (isUpdate.value && paths.value.length > 0) { for (const path of paths.value) { if (path.startsWith(node.path) && path !== node.path) { console.log('onExpand: Reloading deep path:', path); await loadPathNodes(path, node); } } } } } // 勾选事件 function onCheck(checkedKeys: any, info: any) { checkedKeysRef.value = Array.isArray(checkedKeys) ? checkedKeys : checkedKeys.checked; paths.value = info.checkedNodes.map((node: any) => node.path); console.log('onCheck: checkedKeys:', checkedKeysRef.value, 'paths:', paths.value); } // 选择事件 async function onSelect(selKeys: string[], event: any) { if (selKeys.length > 0 && selectedKeys.value[0] !== selKeys[0]) { selectedKeys.value = [selKeys[0]]; } } // 存储表格数据 function saveData(data: any[]) { if (data.length > 0) { fieldInfoObj.value = data.map(({ key, ...rest }) => rest); } else { fieldInfoObj.value = data; } } // 提交事件 async function handleSubmit() { if (!Array.isArray(paths.value) || paths.value.length === 0 || (paths.value.length === 1 && paths.value[0] === '')) { message.info('请选择文件目录,文件目录不能为空'); return; } try { let values = await validate(); const fieldInfo = JSON.stringify(fieldInfoObj.value); for (const obj of fieldInfoObj.value) { for (const key of Object.keys(obj)) { const value = obj[key]; if (value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)) { message.info('表格数据不能为空'); return; } } } let configInfoObj = { host: values.host, port: values.port, username: values.username, password: values.password, os: values.os, paths: paths.value, fileNameMatchingRules: values.fileNameMatchingRules, matchKey: values.matchKey, fileType: values.fileType, readRow: values.readRow, }; const configInfo = JSON.stringify(configInfoObj); let params = { ...values, fieldInfo: fieldInfo, fieldInfoObj: fieldInfoObj.value, configInfoObj: configInfoObj, configInfo: configInfo, }; await saveOrUpdate(params, unref(isUpdate)); closeModal(); emit('success'); } finally { } } // 模态框可见性变化 function visibleChange(visible: boolean) { if (!visible) { treeData.value = []; checkedKeysRef.value = []; paths.value = []; expandedKeys.value = []; selectedKeys.value = []; } } async function createTree() { console.log('createTree: Starting fresh tree generation'); const values = await getFieldsValue(); const params = { host: values.host, port: values.port, username: values.username, password: values.password, os: values.os, path: '/', // 始终从根目录开始 }; if (!values.host || !values.port || !values.username || !values.password || !values.os) { message.info('请填写完整FTP服务器配置再获取目录树'); return; } try { loading.value = true; // 清空所有缓存数据 treeData.value = []; checkedKeysRef.value = []; expandedKeys.value = []; selectedKeys.value = []; const data = await dirList(params); console.log('createTree: Fresh tree data:', data); if (!data || data.length === 0) { message.error('目录树数据为空,请检查FTP服务器配置'); return; } // 生成全新树结构 treeData.value = data.map((item, index) => formatTreeData(item, index)); console.log('createTree: New treeData:', treeData.value); // 自动展开根节点(可选) if (treeData.value.length > 0) { expandedKeys.value = [treeData.value[0].key]; } } catch (error) { console.error('createTree: Error:', error); message.error('获取目录树失败:' + error.message); } finally { loading.value = false; reloadTree(); // 确保UI刷新 } } </script> <style scoped lang="less"> .item { margin-left: 10px; position: relative; .title-view { padding-bottom: 20px 20px 0 20px; display: flex; align-items: center; .tag { width: 5px; height: 18px; border-radius: 2rpx; background: #40a9ff; } .title { padding-left: 7px; font-size: 14px; font-weight: bold; } } } </style>当树树存在数据时,表单的values.host,values.port,values.username,values.password某一项发生修改则提示FTP服务器配置,重新获取目录树,并清空树的数据和paths.value,给我修改后的全部代码,用中文回答
最新发布
06-05
<think>根据用户需求,需要在表单的四个字段(host、port、username、password)中任意一个发生变化时,检测到变化并执行以下操作:1.提示用户重新获取FTP服务器配置(可以通过弹窗、提示信息等方式)2.清空树的数据(这里树的数据应该是存储的FTP目录结构等)3.清空paths.value(应该是存储的路径信息)由于是在前端框架(如Vue、React等)中实现,这里我们以Vue3的组合式API(CompositionAPI)为例,因为用户之前的问题可能是基于Vue3的。但注意,用户没有明确框架,所以我们将重点放在逻辑上。实现思路:1.使用watchEffect或watch来监听表单的这四个字段。由于这四个字段都是响应式的,我们可以将它们组合成一个响应式对象,或者分别监听,但更高效的是将它们放在一个对象里,然后监听该对象的深度变化。2.但是注意:用户要求是当这四个字段中任意一个发生变化时,就触发操作。同时,需要避免在初始化时触发(因为初始赋值可能也会触发watch)。所以我们需要记录上一次的值,或者使用一个标志位来避免初始触发。3.然而,用户特别指出“当树存在数据时”,也就是说,只有当树有数据的情况下,修改这四个字段才需要触发操作。所以我们需要判断树的数据是否为空,不为空时才触发。4.触发操作包括:-提示用户(例如,使用一个弹窗,或者控制台输出?但一般是UI提示)。这里我们可以调用一个提示函数,比如`message.warning`(如果使用AntDesignVue)或者自己写的提示方法。-清空树的数据(将存储树数据的变量重置为空数组或者null)-清空paths.value(将存储路径的变量重置为空数组或者初始值)5.注意:为了避免频繁触发,可以考虑使用深度监听和防抖?但是用户没有要求,所以暂时不考虑。具体代码实现(Vue3CompositionAPI):假设我们的表单数据使用`reactive`或`ref`创建,树数据为`treeData`,路径数据为`paths`(也是响应式的)。步骤:a.定义表单响应式对象(如`formValues`),包含host,port,username,password。b.定义树数据(`treeData`)和路径数据(`paths`),均为响应式。c.使用watch监听`formValues`的这四个属性,由于有多个属性,我们可以使用深度监听,或者使用一个计算属性将它们组合起来,然后监听这个计算属性。由于Vue3的watch可以监听一个getter函数,我们可以这样写:```javascriptimport{watch,reactive,ref}from'vue';//表单数据constformValues=reactive({host:'',port:'',username:'',password:''});//树数据consttreeData=ref([]);//假设树数据是一个数组constpaths=ref([]);//路径数据//监听表单的变化watch(()=>({...formValues}),//创建一个新对象,包含这四个属性,这样当任何一个变化时,这个新对象就会变化(注意:这里简单使用浅拷贝,因为属性都是基本类型)(newValues,oldValues)=>{//注意:第一次执行时,oldValues和newValues相同,但用户要求是在变化时触发,而且要求树数据存在的情况下,所以我们还需要检查treeData是否为空//我们需要判断treeData是否有数据,如果有,再触发if(treeData.value&&treeData.value.length>0){//检查这四个属性中是否有变化(因为监听的是整个对象,只要任何一个变化都会触发,但为了精确,我们可以对比新旧值中的这四个字段,但实际上由于监听的是整个对象,所以任何一个变化都会触发,且新旧值不同)//由于我们重新创建了一个对象,所以每次都会触发,但为了避免初始化就触发,我们可以加一个标志位,或者判断oldValues是否为空?但这里我们可以通过treeData的存在来判断需要执行操作//提示用户//这里使用一个提示方法,比如:message.warning('检测到连接配置发生变化,需要重新获取FTP服务器配置');//清空树数据treeData.value=[];//清空路径数据paths.value=[];}},{deep:true}//实际上这里不需要deep,因为我们拷贝的是第一层,而且属性是基本类型);```但是注意:上面的监听函数中,我们使用`()=>({...formValues})`来返回一个新对象,这样当formValues的任何属性变化时,这个新对象就会变化(因为每次返回的都是一个新的对象,而watch默认是浅比较,所以会认为变化)。但是这样会有一个问题:如果同时改变多个属性,也只会触发一次。这符合预期。然而,上面的代码会在第一次初始化赋值时也触发,因为第一次watch会将初始值记录下来,然后当表单字段从undefined变成初始值(比如空字符串)时,也会触发。但我们通过`treeData.value.length>0`来避免了初始触发(因为初始时树数据是空的)。另一种方式是使用`immediate:false`(默认就是false)来避免初始化触发,但我们的监听函数会在第一次赋值后不会被调用?不对,watch默认不会在初始化时运行,除非设置immediate:true。所以上面代码在初始化时不会触发,因为watch在初始时不执行。但注意:如果formValues的初始值是在watch之前设置的,那么watch第一次触发会在下一次数据变化时。所以我们的逻辑应该是:在设置初始值后,然后当用户修改表单字段时触发。然而,还有一种情况:如果用户修改了某个字段,然后又改回来,这样两次的值是一样的,但中间触发了一次?实际上,由于我们监听的是整个对象,每次都是新对象,所以即使值相同,对象引用不同,也会触发。为了避免这种情况,我们可以比较新旧值中这四个字段是否真的有变化(但用户没有要求,所以可以不处理,因为即使触发,也只是提示和清空,而树数据已经被清空过一次,再次清空也没关系,但提示会再次出现)。但是,如果树数据已经被清空了(即上一次变化已经清空),那么再次触发时,因为treeData为空,就不会再执行操作。所以不会重复提示。改进:我们可以在执行操作之前,检查一下这四个字段中是否至少有一个发生了变化(比较新旧值)?但是上面的代码中,我们并没有使用oldValues和newValues来比较,而是只要触发且树有数据就执行。所以如果用户连续修改,第一次修改了host,触发了操作,树被清空;然后修改port,再次触发时,树已经空了,所以不会执行操作。因此,提示只会出现一次(当第一次修改时),后续修改不会触发操作,直到用户重新获取了树数据(treeData被重新赋值)后,再次修改表单字段才会触发。但是,如果用户同时修改两个字段(比如一次输入中修改了两个字段),那么只会触发一次,提示一次。所以,上面的代码基本满足要求。注意:用户可能希望提示用户后,用户可以选择是否立即去重新获取,或者只是提醒。上面的代码只是提示和自动清空数据,具体重新获取的操作需要用户手动触发(如点击连接按钮等)。所以符合需求。如果是React,我们可以用useEffect配合useRef来记录上一次的表单值,但逻辑类似。现在,我们将其放入一个Vue组件中的setup部分。提示:用户可能使用了某个UI库,比如ElementPlus或者AntDesignVue,我们这里使用通用的方法,提示方法用console.warn代替,实际中替换为相应的UI组件方法。完整示例:```vue<scriptsetup>import{reactive,ref,watch}from'vue';constformValues=reactive({host:'',port:'',username:'',password:''});consttreeData=ref([]);constpaths=ref([]);//监听表单的变化watch(()=>({...formValues}),(newVal,oldVal)=>{//如果树数据存在(有数据)if(treeData.value.length>0){//提示用户console.warn('连接配置已更改,请重新获取FTP服务器配置。');//或者使用UI组件的提示//message.warning('连接配置已更改,请重新获取FTP服务器配置。');//清空树数据treeData.value=[];//清空路径数据paths.value=[];}},{deep:false}//其实这里deep:false也可以,因为我们只监听第一层的基本类型);</script>```注意事项:1.如果表单数据中某个字段是对象,那么我们需要深度监听,但这里都是字符串,所以浅监听即可。2.使用`{...formValues}`来创建一个新对象,这样每次都能触发(因为新对象和旧对象引用不同)。另一种写法(监听多个源):我们可以单独监听这四个字段,但是这样写四个watch比较麻烦,或者使用一个watch监听一个数组:```javascriptwatch([()=>formValues.host,()=>formValues.port,()=>formValues.username,()=>formValues.password],(newValues,oldValues)=>{//同样,检查treeData是否有数据if(treeData.value.length>0){//提示并清空console.warn('连接配置已更改,请重新获取FTP服务器配置。');treeData.value=[];paths.value=[];}});```这样,任何一个变化都会触发。但是,这个watch会在初始化时触发一次(因为Vue3的watch在监听多个源时,默认immediate为false,所以初始化不会触发,但是第一次变化会触发,而且每个字段的变化都会触发,但可能会触发多次?比如连续修改多个字段,就会触发多次)。所以我们需要加上防抖?但是用户没有要求,而且多次触发在逻辑上也没问题(因为第一次触发后treeData就被清空,后续触发就不会执行了)。所以两种方法都可以。但推荐第一种,因为只触发一次(一次事件循环内的多次修改会被合并,但Vue的watch默认不会合并,所以第一种方法中,如果同时修改多个字段,会触发多次?实际上,因为每个字段的修改都会导致重新计算getter,从而触发watch,所以可能会触发多次?但Vue内部会异步执行watch,所以不会立即执行,但是多次修改会导致多次触发。)为了避免多次触发,我们可以使用nextTick或者防抖函数,但是用户需求中没有提到,所以暂时不考虑。综上,我们采用第一种方法,并确保在清空树数据后,后续的修改不会再次触发操作(因为判断了treeData.value.length>0)。最后,在模板部分,我们需要将表单字段绑定到formValues的各个属性(使用v-model)。用户的问题是如何检测变化,我们已经回答了检测的方法。注意:如果用户使用的是OptionsAPI,那么可以使用watch选项来监听formValues的这四个属性,但是写法会不同。由于用户之前的问题可能是基于CompositionAPI的,所以我们这里使用CompositionAPI。相关问题:§§相关问题§§1.在Vue中,除了watch,还有哪些方式可以监听数据变化?2.如何避免在监听数据变化时重复触发操作?3.在React中如何实现类似的监听效果?</think> 为了在检测到表单的 `values.host`、`values.port`、`values.username` 或 `values.password` 中的任何一项变化时触发操作,可以使用以下方案(以 Vue 3 Composition API 为例): ### 实现方案 ```vue <script setup> import { ref, watch, reactive } from 'vue' const treeData = ref([]) // FTP目录树数据 const paths = ref([]) // 存储的路径数据 const values = reactive({ host: '', port: '', username: '', password: '' }) // 监听表单关键字段的变化 watch( () => [ values.host, values.port, values.username, values.password ], (newVals, oldVals) => { // 仅在树数据存在时触发 if (treeData.value.length > 0 || paths.value.length > 0) { // 检查实际值是否发生变化 const isChanged = newVals.some( (val, i) => val !== oldVals[i] ) if (isChanged) { // 1. 提示用户重新获取配置 alert('连接配置已更改,请重新获取FTP服务器配置!') // 2. 清空树数据 treeData.value = [] // 3. 清空路径数据 paths.value = [] } } }, { deep: true } // 深度监听对象内部变化 ) </script> ``` ### 关键实现说明 1. **变化检测逻辑**: - 使用 `watch` 监听4个关键字段组成的数组 - 比较新旧值数组判断是否发生真实更改(避免误触发) - `treeData.value.length > 0` 确保只在树存在数据时触发 2. **清理操作**: - `treeData.value = []` 清空树结构数据 - `paths.value = []` 清空路径数据 - 通过 UI 提示提醒用户需要重新获取配置 3. **优化选项**: - `{ deep: true }` 确保深度监听对象属性变化 - 添加 `isChanged` 检查避免相同值重复触发 ### 不同框架的实现要点 | 框架 | 监听方式 | 注意事项 | |------------|-------------------------------|-----------------------------| | **Vue 2** | `watch: { 'values.*'() {} }` | 需要为每个字段单独设置侦听器 | | **React** | `useEffect` 依赖数组 | 需要包含所有依赖字段 | | **Svelte** | `$: if(values) {}` | 响应式语句自动追踪依赖 | ### 实际应用建议 1. **用户提示优化**: ```javascript // 替换 alert 为更友好的 UI 通知 import { useToast } from 'vue-toastification' const toast = useToast() toast.warning('连接配置已更改,请重新获取服务器数据!') ``` 2. **防抖处理**(避免频繁操作): ```javascript watch( // ... { deep: true, flush: 'post', onTrigger(e) { clearTimeout(debounceTimer) debounceTimer = setTimeout(executeClear, 300) } } ) ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值