如何限制NSTextField文本长度并始终保持大写?

时间:2021-02-01 14:56:46

Need to have an NSTextField with a text limit of 4 characters maximum and show always in upper case but can't figure out a good way of achieving that. I've tried to do it through a binding with a validation method but the validation only gets called when the control loses first responder and that's no good.

需要一个NSTextField,其文本限制最多为4个字符,并且总是以大写形式显示,但无法找到实现这一目标的好方法。我试图通过与验证方法的绑定来实现它,但只有在控件失去第一个响应者时才会调用验证,这是不好的。

Temporarly I made it work by observing the notification NSControlTextDidChangeNotification on the text field and having it call the method:

暂时我通过在文本字段上观察通知NSControlTextDidChangeNotification并让它调用方法来使其工作:

- (void)textDidChange:(NSNotification*)notification {
  NSTextField* textField = [notification object];
  NSString* value = [textField stringValue];
  if ([value length] > 4) {
    [textField setStringValue:[[value uppercaseString] substringWithRange:NSMakeRange(0, 4)]];
  } else {
    [textField setStringValue:[value uppercaseString]];
  }
}

But this surely isn't the best way of doing it. Any better suggestion?

但这肯定不是最好的方法。还有更好的建议吗?

6 个解决方案

#1


44  

I did as Graham Lee suggested and it works fine, here's the custom formatter code:

我像Graham Lee建议的那样做了,它工作正常,这是自定义格式化代码:

UPDATED: Added fix reported by Dave Gallagher. Thanks!

更新:添加Dave Gallagher报道的修复程序。谢谢!

@interface CustomTextFieldFormatter : NSFormatter {
  int maxLength;
}
- (void)setMaximumLength:(int)len;
- (int)maximumLength;

@end

@implementation CustomTextFieldFormatter

- (id)init {

   if(self = [super init]){

      maxLength = INT_MAX;
   }

  return self;
}

- (void)setMaximumLength:(int)len {
  maxLength = len;
}

- (int)maximumLength {
  return maxLength;
}

- (NSString *)stringForObjectValue:(id)object {
  return (NSString *)object;
}

- (BOOL)getObjectValue:(id *)object forString:(NSString *)string errorDescription:(NSString **)error {
  *object = string;
  return YES;
}

- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
   proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
          originalString:(NSString *)origString
   originalSelectedRange:(NSRange)origSelRange
        errorDescription:(NSString **)error {
    if ([*partialStringPtr length] > maxLength) {
        return NO;
    }

    if (![*partialStringPtr isEqual:[*partialStringPtr uppercaseString]]) {
      *partialStringPtr = [*partialStringPtr uppercaseString];
      return NO;
    }

    return YES;
}

- (NSAttributedString *)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attributes {
  return nil;
}

@end

#2


12  

Have you tried attaching a custom NSFormatter subclass?

您是否尝试附加自定义NSFormatter子类?

#3


11  

In the above example where I commented, this is bad:

在我评论的上述示例中,这很糟糕:

// Don't use:
- (BOOL)isPartialStringValid:(NSString *)partialString
            newEditingString:(NSString **)newString
            errorDescription:(NSString **)error
{
    if ((int)[partialString length] > maxLength)
    {
        *newString = nil;
        return NO;
    }
}

Use this (or something like it) instead:

使用此(或类似的东西)代替:

// Good to use:
- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
       proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
              originalString:(NSString *)origString
       originalSelectedRange:(NSRange)origSelRange
            errorDescription:(NSString **)error
{
    int size = [*partialStringPtr length];
    if ( size > maxLength )
    {
        return NO;
    }
    return YES;
}

Both are NSFormatter methods. The first one has an issue. Say you limit text-entry to 10 characters. If you type characters in one-by-one into an NSTextField, it'll work fine and prevent users from going beyond 10 characters.

两者都是NSFormatter方法。第一个有问题。假设您将文本输入限制为10个字符。如果您将字符逐个键入NSTextField,它将正常工作并防止用户超过10个字符。

However, if a user was to paste a string of, say, 25 characters into the Text Field, what'll happen is something like this:

但是,如果用户要将一串(例如25个字符)粘贴到文本字段中,那么会发生以下情况:

1) User will paste into TextField

1)用户将粘贴到TextField中

2) TextField will accept the string of characters

2)TextField将接受字符串

3) TextField will apply the formatter to the "last" character in the 25-length string

3)TextField将格式化程序应用于25长度字符串中的“最后”字符

4) Formatter does stuff to the "last" character in the 25-length string, ignoring the rest

4)Formatter对25长度字符串中的“last”字符进行填充,忽略其余字符

5) TextField will end up with 25 characters in it, even though it's limited to 10.

5)TextField最终会有25个字符,即使它只限于10个字符。

This is because, I believe, the first method only applies to the "very last character" typed into an NSTextField. The second method shown above applies to "all characters" typed into the NSTextField. So it's immune to the "paste" exploit.

这是因为,我认为,第一种方法仅适用于键入NSTextField的“最后一个字符”。上面显示的第二种方法适用于键入NSTextField的“所有字符”。所以它不受“粘贴”攻击的影响。

I discovered this just now trying to break my application, and am not an expert on NSFormatter, so please correct me if I'm wrong. And very much thanks to you carlosb for posting that example. It helped a LOT! :)

我刚刚发现这个试图破坏我的应用程序,并且不是NSFormatter的专家,所以如果我错了请纠正我。非常感谢carlosb发布这个例子。它帮了很多! :)

#4


9  

This implementation adopts several of the suggestions commented on above. Notably it works correctly with continuously updating bindings.

该实现采用了上面提到的几个建议。值得注意的是,它可以正常连续更新绑定。

In addition:

此外:

  1. It implements paste correctly.

    它正确实现了粘贴。

  2. It includes some notes on how to use the class effectively in a nib without further subclassing.

    它包含一些关于如何在nib中有效使用该类而无需进一步子类化的注释。

The code:

代码:

@interface BPPlainTextFormatter : NSFormatter {
    NSInteger _maxLength;
}


/*

 Set the maximum string length. 

 Note that to use this class within a Nib:
 1. Add an NSFormatter as a Custom Formatter.
 2. In the Identity inspector set the Class to BPPlainTextFormatter
 3. In user defined attributes add Key Path: maxLength Type: Number Value: 30

 Note that rather than attaching formatter instances to individual cells they
 can be positioned in the nib Objects section and referenced by numerous controls.
 A name, such as Plain Text Formatter 100, can  be used to identify the formatters max length.

 */
@property NSInteger maxLength;

@end


@implementation BPPlainTextFormatter
@synthesize maxLength = _maxLength;

- (id)init
{
    if(self = [super init]){
        self.maxLength = INT_MAX;
    }

    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    // support Nib based initialisation
    self = [super initWithCoder:aDecoder];
    if (self) {
        self.maxLength = INT_MAX;
    }

    return self;
}

#pragma mark -
#pragma mark Textual Representation of Cell Content

- (NSString *)stringForObjectValue:(id)object
{
    NSString *stringValue = nil;
    if ([object isKindOfClass:[NSString class]]) {

        // A new NSString is perhaps not required here
        // but generically a new object would be generated
        stringValue = [NSString stringWithString:object];
    }

    return stringValue;
}

#pragma mark -
#pragma mark Object Equivalent to Textual Representation

- (BOOL)getObjectValue:(id *)object forString:(NSString *)string errorDescription:(NSString **)error
{
    BOOL valid = YES;

    // Be sure to generate a new object here or binding woe ensues
    // when continuously updating bindings are enabled.
    *object = [NSString stringWithString:string];

    return valid;
}

#pragma mark -
#pragma mark Dynamic Cell Editing

- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
       proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
              originalString:(NSString *)origString
       originalSelectedRange:(NSRange)origSelRange
            errorDescription:(NSString **)error
{
    BOOL valid = YES;

    NSString *proposedString = *partialStringPtr;
    if ([proposedString length] > self.maxLength) {

        // The original string has been modified by one or more characters (via pasting).
        // Either way compute how much of the proposed string can be accommodated.
        NSInteger origLength = origString.length;
        NSInteger insertLength = self.maxLength - origLength;

        // If a range is selected then characters in that range will be removed
        // so adjust the insert length accordingly
        insertLength += origSelRange.length;

        // Get the string components
        NSString *prefix = [origString substringToIndex:origSelRange.location];
        NSString *suffix = [origString substringFromIndex:origSelRange.location + origSelRange.length];
        NSString *insert = [proposedString substringWithRange:NSMakeRange(origSelRange.location, insertLength)];

#ifdef _TRACE

        NSLog(@"Original string: %@", origString);
        NSLog(@"Original selection location: %u length %u", origSelRange.location, origSelRange.length);

        NSLog(@"Proposed string: %@", proposedString);
        NSLog(@"Proposed selection location: %u length %u", proposedSelRangePtr->location, proposedSelRangePtr->length);

        NSLog(@"Prefix: %@", prefix);
        NSLog(@"Suffix: %@", suffix);
        NSLog(@"Insert: %@", insert);
#endif

        // Assemble the final string
        *partialStringPtr = [[NSString stringWithFormat:@"%@%@%@", prefix, insert, suffix] uppercaseString];

        // Fix-up the proposed selection range
        proposedSelRangePtr->location = origSelRange.location + insertLength;
        proposedSelRangePtr->length = 0;

#ifdef _TRACE

        NSLog(@"Final string: %@", *partialStringPtr);
        NSLog(@"Final selection location: %u length %u", proposedSelRangePtr->location, proposedSelRangePtr->length);

#endif
        valid = NO;
    }

    return valid;
}

@end

#5


2  

I needed a Formatter to convert to uppercase for Swift 4. For reference I've included it here:

我需要一个Formatter转换为Swift 4的大写。作为参考,我在这里包含它:

import Foundation

class UppercaseFormatter : Formatter {

    override func string(for obj: Any?) -> String? {
        if let stringValue = obj as? String {
            return stringValue.uppercased()
        }
        return nil
    }

    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
        obj?.pointee = string as AnyObject
        return true
    }
}

#6


-1  

The custom NSFormatter that Graham Lee suggested is the best approach.

Graham Lee建议的自定义NSFormatter是最好的方法。

A simple kludge would be to set your view controller as the text field's delegate then just block any edit that involves non-uppercase or makes the length longer than 4:

一个简单的方法是将视图控制器设置为文本字段的委托,然后阻止任何涉及非大写的编辑或使长度超过4:

- (BOOL)textField:(UITextField *)textField
    shouldChangeCharactersInRange:(NSRange)range
    replacementString:(NSString *)string
{
    NSMutableString *newValue = [[textField.text mutableCopy] autorelease];
    [newValue replaceCharactersInRange:range withString:string];

    NSCharacterSet *nonUppercase =
        [[NSCharacterSet uppercaseLetterCharacterSet] invertedSet];
    if ([newValue length] > 4 ||
        [newValue rangeOfCharacterFromSet:nonUppercase].location !=
            NSNotFound)
    {
       return NO;
    }

    return YES;
}

#1


44  

I did as Graham Lee suggested and it works fine, here's the custom formatter code:

我像Graham Lee建议的那样做了,它工作正常,这是自定义格式化代码:

UPDATED: Added fix reported by Dave Gallagher. Thanks!

更新:添加Dave Gallagher报道的修复程序。谢谢!

@interface CustomTextFieldFormatter : NSFormatter {
  int maxLength;
}
- (void)setMaximumLength:(int)len;
- (int)maximumLength;

@end

@implementation CustomTextFieldFormatter

- (id)init {

   if(self = [super init]){

      maxLength = INT_MAX;
   }

  return self;
}

- (void)setMaximumLength:(int)len {
  maxLength = len;
}

- (int)maximumLength {
  return maxLength;
}

- (NSString *)stringForObjectValue:(id)object {
  return (NSString *)object;
}

- (BOOL)getObjectValue:(id *)object forString:(NSString *)string errorDescription:(NSString **)error {
  *object = string;
  return YES;
}

- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
   proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
          originalString:(NSString *)origString
   originalSelectedRange:(NSRange)origSelRange
        errorDescription:(NSString **)error {
    if ([*partialStringPtr length] > maxLength) {
        return NO;
    }

    if (![*partialStringPtr isEqual:[*partialStringPtr uppercaseString]]) {
      *partialStringPtr = [*partialStringPtr uppercaseString];
      return NO;
    }

    return YES;
}

- (NSAttributedString *)attributedStringForObjectValue:(id)anObject withDefaultAttributes:(NSDictionary *)attributes {
  return nil;
}

@end

#2


12  

Have you tried attaching a custom NSFormatter subclass?

您是否尝试附加自定义NSFormatter子类?

#3


11  

In the above example where I commented, this is bad:

在我评论的上述示例中,这很糟糕:

// Don't use:
- (BOOL)isPartialStringValid:(NSString *)partialString
            newEditingString:(NSString **)newString
            errorDescription:(NSString **)error
{
    if ((int)[partialString length] > maxLength)
    {
        *newString = nil;
        return NO;
    }
}

Use this (or something like it) instead:

使用此(或类似的东西)代替:

// Good to use:
- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
       proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
              originalString:(NSString *)origString
       originalSelectedRange:(NSRange)origSelRange
            errorDescription:(NSString **)error
{
    int size = [*partialStringPtr length];
    if ( size > maxLength )
    {
        return NO;
    }
    return YES;
}

Both are NSFormatter methods. The first one has an issue. Say you limit text-entry to 10 characters. If you type characters in one-by-one into an NSTextField, it'll work fine and prevent users from going beyond 10 characters.

两者都是NSFormatter方法。第一个有问题。假设您将文本输入限制为10个字符。如果您将字符逐个键入NSTextField,它将正常工作并防止用户超过10个字符。

However, if a user was to paste a string of, say, 25 characters into the Text Field, what'll happen is something like this:

但是,如果用户要将一串(例如25个字符)粘贴到文本字段中,那么会发生以下情况:

1) User will paste into TextField

1)用户将粘贴到TextField中

2) TextField will accept the string of characters

2)TextField将接受字符串

3) TextField will apply the formatter to the "last" character in the 25-length string

3)TextField将格式化程序应用于25长度字符串中的“最后”字符

4) Formatter does stuff to the "last" character in the 25-length string, ignoring the rest

4)Formatter对25长度字符串中的“last”字符进行填充,忽略其余字符

5) TextField will end up with 25 characters in it, even though it's limited to 10.

5)TextField最终会有25个字符,即使它只限于10个字符。

This is because, I believe, the first method only applies to the "very last character" typed into an NSTextField. The second method shown above applies to "all characters" typed into the NSTextField. So it's immune to the "paste" exploit.

这是因为,我认为,第一种方法仅适用于键入NSTextField的“最后一个字符”。上面显示的第二种方法适用于键入NSTextField的“所有字符”。所以它不受“粘贴”攻击的影响。

I discovered this just now trying to break my application, and am not an expert on NSFormatter, so please correct me if I'm wrong. And very much thanks to you carlosb for posting that example. It helped a LOT! :)

我刚刚发现这个试图破坏我的应用程序,并且不是NSFormatter的专家,所以如果我错了请纠正我。非常感谢carlosb发布这个例子。它帮了很多! :)

#4


9  

This implementation adopts several of the suggestions commented on above. Notably it works correctly with continuously updating bindings.

该实现采用了上面提到的几个建议。值得注意的是,它可以正常连续更新绑定。

In addition:

此外:

  1. It implements paste correctly.

    它正确实现了粘贴。

  2. It includes some notes on how to use the class effectively in a nib without further subclassing.

    它包含一些关于如何在nib中有效使用该类而无需进一步子类化的注释。

The code:

代码:

@interface BPPlainTextFormatter : NSFormatter {
    NSInteger _maxLength;
}


/*

 Set the maximum string length. 

 Note that to use this class within a Nib:
 1. Add an NSFormatter as a Custom Formatter.
 2. In the Identity inspector set the Class to BPPlainTextFormatter
 3. In user defined attributes add Key Path: maxLength Type: Number Value: 30

 Note that rather than attaching formatter instances to individual cells they
 can be positioned in the nib Objects section and referenced by numerous controls.
 A name, such as Plain Text Formatter 100, can  be used to identify the formatters max length.

 */
@property NSInteger maxLength;

@end


@implementation BPPlainTextFormatter
@synthesize maxLength = _maxLength;

- (id)init
{
    if(self = [super init]){
        self.maxLength = INT_MAX;
    }

    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    // support Nib based initialisation
    self = [super initWithCoder:aDecoder];
    if (self) {
        self.maxLength = INT_MAX;
    }

    return self;
}

#pragma mark -
#pragma mark Textual Representation of Cell Content

- (NSString *)stringForObjectValue:(id)object
{
    NSString *stringValue = nil;
    if ([object isKindOfClass:[NSString class]]) {

        // A new NSString is perhaps not required here
        // but generically a new object would be generated
        stringValue = [NSString stringWithString:object];
    }

    return stringValue;
}

#pragma mark -
#pragma mark Object Equivalent to Textual Representation

- (BOOL)getObjectValue:(id *)object forString:(NSString *)string errorDescription:(NSString **)error
{
    BOOL valid = YES;

    // Be sure to generate a new object here or binding woe ensues
    // when continuously updating bindings are enabled.
    *object = [NSString stringWithString:string];

    return valid;
}

#pragma mark -
#pragma mark Dynamic Cell Editing

- (BOOL)isPartialStringValid:(NSString **)partialStringPtr
       proposedSelectedRange:(NSRangePointer)proposedSelRangePtr
              originalString:(NSString *)origString
       originalSelectedRange:(NSRange)origSelRange
            errorDescription:(NSString **)error
{
    BOOL valid = YES;

    NSString *proposedString = *partialStringPtr;
    if ([proposedString length] > self.maxLength) {

        // The original string has been modified by one or more characters (via pasting).
        // Either way compute how much of the proposed string can be accommodated.
        NSInteger origLength = origString.length;
        NSInteger insertLength = self.maxLength - origLength;

        // If a range is selected then characters in that range will be removed
        // so adjust the insert length accordingly
        insertLength += origSelRange.length;

        // Get the string components
        NSString *prefix = [origString substringToIndex:origSelRange.location];
        NSString *suffix = [origString substringFromIndex:origSelRange.location + origSelRange.length];
        NSString *insert = [proposedString substringWithRange:NSMakeRange(origSelRange.location, insertLength)];

#ifdef _TRACE

        NSLog(@"Original string: %@", origString);
        NSLog(@"Original selection location: %u length %u", origSelRange.location, origSelRange.length);

        NSLog(@"Proposed string: %@", proposedString);
        NSLog(@"Proposed selection location: %u length %u", proposedSelRangePtr->location, proposedSelRangePtr->length);

        NSLog(@"Prefix: %@", prefix);
        NSLog(@"Suffix: %@", suffix);
        NSLog(@"Insert: %@", insert);
#endif

        // Assemble the final string
        *partialStringPtr = [[NSString stringWithFormat:@"%@%@%@", prefix, insert, suffix] uppercaseString];

        // Fix-up the proposed selection range
        proposedSelRangePtr->location = origSelRange.location + insertLength;
        proposedSelRangePtr->length = 0;

#ifdef _TRACE

        NSLog(@"Final string: %@", *partialStringPtr);
        NSLog(@"Final selection location: %u length %u", proposedSelRangePtr->location, proposedSelRangePtr->length);

#endif
        valid = NO;
    }

    return valid;
}

@end

#5


2  

I needed a Formatter to convert to uppercase for Swift 4. For reference I've included it here:

我需要一个Formatter转换为Swift 4的大写。作为参考,我在这里包含它:

import Foundation

class UppercaseFormatter : Formatter {

    override func string(for obj: Any?) -> String? {
        if let stringValue = obj as? String {
            return stringValue.uppercased()
        }
        return nil
    }

    override func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?, for string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
        obj?.pointee = string as AnyObject
        return true
    }
}

#6


-1  

The custom NSFormatter that Graham Lee suggested is the best approach.

Graham Lee建议的自定义NSFormatter是最好的方法。

A simple kludge would be to set your view controller as the text field's delegate then just block any edit that involves non-uppercase or makes the length longer than 4:

一个简单的方法是将视图控制器设置为文本字段的委托,然后阻止任何涉及非大写的编辑或使长度超过4:

- (BOOL)textField:(UITextField *)textField
    shouldChangeCharactersInRange:(NSRange)range
    replacementString:(NSString *)string
{
    NSMutableString *newValue = [[textField.text mutableCopy] autorelease];
    [newValue replaceCharactersInRange:range withString:string];

    NSCharacterSet *nonUppercase =
        [[NSCharacterSet uppercaseLetterCharacterSet] invertedSet];
    if ([newValue length] > 4 ||
        [newValue rangeOfCharacterFromSet:nonUppercase].location !=
            NSNotFound)
    {
       return NO;
    }

    return YES;
}