IOS微软语音转文本,lame压缩音频

news2024/11/16 4:39:22

在IOS开发中,用微软进行语音转文本操作,并将录音文件压缩后返回

项目中遇到了利用微软SDK进行实时录音转文本操作,如果操作失败,那么就利用原始音频文件通过网络请求操作,最终这份文件上传到阿里云保存,考虑到传输速率,对文件压缩成mp3再上传

遇到的难点

  • 微软的示例中只能转文本,微软并不保存这份音频文件,需要自己实现从录音到推流,到获取结果
  • 项目是uniapp项目,非原生工程项目,录音管理器需要激活后才能使用
  • 关于压缩代码,采用Lame库压缩,网上大部分都是通过文件提取压缩再保存,直接录制音频压缩较少,记录下来以便后续使用

流程图

请添加图片描述

实现步骤

录音的实现

// 每个缓冲区的大小
#define kBufferSize 2048
// 缓冲区数量
#define kNumberBuffers 3

// 定义结构体,里面保存录音队列ID,录音格式
typedef struct {
    AudioStreamBasicDescription dataFormat;
    AudioQueueRef               queue;
    AudioQueueBufferRef         buffers[kNumberBuffers];
    UInt32                      bufferByteSize;
    __unsafe_unretained id      selfRef;
} AQRecorderState;

AQRecorderState recorderState = {0};

- (instancetype)init {
    self = [super init];
    if (self) {
        // 设置音频格式
        recorderState.dataFormat.mFormatID = kAudioFormatLinearPCM;
        recorderState.dataFormat.mSampleRate = 16000.0;
        recorderState.dataFormat.mChannelsPerFrame = 1;
        recorderState.dataFormat.mBitsPerChannel = 16;
        recorderState.dataFormat.mBytesPerPacket = recorderState.dataFormat.mBytesPerFrame = recorderState.dataFormat.mChannelsPerFrame * sizeof(SInt16);
        recorderState.dataFormat.mFramesPerPacket = 1;
        recorderState.dataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
    }
    return self;
}

- (void)configureAudioSession {
    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSError *error = nil;
    
    // 设置音频会话类别和模式
    [session setCategory:AVAudioSessionCategoryPlayAndRecord  error:&error];
    
    if (error) {
        NSLog(@"Error setting category: %@", error.localizedDescription);
    }
    
    // 激活音频会话
    [session setActive:YES error:&error];
    if (error) {
        NSLog(@"Error activating session: %@", error.localizedDescription);
    }
}
// 开始录音
- (void)startRecording{
	// 激活录音文件
	 [self configureAudioSession];
    // 创建录音队列
    AudioQueueNewInput(&recorderState.dataFormat, HandleInputBuffer, &recorderState, NULL, kCFRunLoopCommonModes, 0, &recorderState.queue);
    // 设置录音增益
    AudioQueueSetParameter(recorderState.queue, kAudioQueueParam_Volume, 1.0);
    // 计算缓冲区大小
    DeriveBufferSize(recorderState.queue, &recorderState.dataFormat, 0.5, &recorderState.bufferByteSize);
    
    // 分配和分配缓冲区
    for (int i = 0; i < kNumberBuffers; i++) {
        AudioQueueAllocateBuffer(recorderState.queue, recorderState.bufferByteSize, &recorderState.buffers[i]);
        AudioQueueEnqueueBuffer(recorderState.queue, recorderState.buffers[i], 0, NULL);
    }
	OSStatus status = AudioQueueStart(recorderState.queue, NULL);
    if (status != noErr) {
        NSLog(@"AudioQueueNewInput failed with error: %d", (int)status);
    }
}
// 结束录音
- (void)stopRecording{
    // 停止录音
    AudioQueueStop(recorderState.queue, true);
    AudioQueueDispose(recorderState.queue, true);
};
// 计算缓冲区大小
void DeriveBufferSize(AudioQueueRef audioQueue, AudioStreamBasicDescription *ASBDesc, Float64 seconds, UInt32 *outBufferSize) {
    
    static const int maxBufferSize = 0x50000; // 限制缓冲区的最大值
    int maxPacketSize = ASBDesc->mBytesPerPacket;
    if (maxPacketSize == 0) {
        UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
        AudioQueueGetProperty(audioQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize, &maxVBRPacketSize);
    }
    Float64 numBytesForTime = ASBDesc->mSampleRate * maxPacketSize * seconds;
    *outBufferSize = (UInt32)(numBytesForTime < maxBufferSize ? numBytesForTime : maxBufferSize);
}
// 数据处理回调函数 这个里面有个录音回掉的PCM数据
void HandleInputBuffer(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) {
    AQRecorderState *pAqData = (AQRecorderState *)aqData;
    
    // 如果有数据,就处理
    if (inNumPackets > 0) {
        // 创建NSData对象
        NSData *audioData = [NSData dataWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize];
        // 打印NSData对象内容
        NSLog(@"Audio Data: %@", audioData);
        // 这儿将进行保存文件
        // 编码文件
        // 推送数据到微软SDK
    }
    
    // 将缓冲区重新加入到队列中
    AudioQueueEnqueueBuffer(pAqData->queue, inBuffer, 0, NULL);
}

微软SDK初始化和推流

这个大部分和微软示例差不多,需要注意的是获取微软示例时候传入的是自定义的录音设置,并将自定义录音设置保存起来,在录音回掉中将数据推入流中

- (void)setUpKey:(NSString *)token service:(NSString *)service lang:(NSString *) lang { 
	// 这里将通过token和区域初始化配置类,微软还有其他获取配置类的方法,其他方法示例化也可以
    SPXSpeechConfiguration *speechConfig = nil;
            speechConfig = [[SPXSpeechConfiguration alloc] initWithAuthorizationToken:token region:service];
            // 这个是通过token实例化配置类的方式
//    speechConfig = [[SPXSpeechConfiguration alloc] initWithSubscription:token region:service];
	// 设置语言 en-US格式
    [speechConfig setSpeechRecognitionLanguage:lang];
    // 设置微软接收到的数据的格式 16000HZ 16位深 单通道
    SPXAudioStreamFormat *audioFormat = [[SPXAudioStreamFormat alloc] initUsingPCMWithSampleRate: 16000 bitsPerSample:16 channels:1];
    // 获取推流的类,并保存起来,后面就通过它推送数据到SDK
    self.audioInputStream = [[SPXPushAudioInputStream alloc] initWithAudioFormat:audioFormat];
    // 获取录音配置
    SPXAudioConfiguration* audioConfig = [[SPXAudioConfiguration alloc] initWithStreamInput:self.audioInputStream];
    // 通过配置类和录音类信息获取微软识别器
    self.recognizer = [[SPXSpeechRecognizer alloc] initWithSpeechConfiguration:speechConfig audioConfiguration:audioConfig];
    // 定义已识别事件的处理函数
       [self.recognizer addRecognizedEventHandler:^(SPXSpeechRecognizer *recognizer, SPXSpeechRecognitionEventArgs *eventArgs) {
        NSString *recognizedText = eventArgs.result.text;
        NSLog(@"Final recognized text: %@", recognizedText);
        // 在这里处理最终识别结果
        [self.speechToTextResult appendFormat:recognizedText];
    }];
    
    // 定义识别中事件的处理函数
    [self.recognizer addRecognizingEventHandler:^(SPXSpeechRecognizer *recognizer, SPXSpeechRecognitionEventArgs *eventArgs) {
        NSString *intermediateText = eventArgs.result.text;
        NSLog(@"Intermediate recognized text: %@", intermediateText);
        // 在这里处理中间识别结果
    }];
    
    // 定义取消事件的处理函数
    [self.recognizer addCanceledEventHandler:^(SPXSpeechRecognizer *recognizer, SPXSpeechRecognitionCanceledEventArgs *eventArgs) {
        NSLog(@"Recognition canceled. Reason: %ld", (long)eventArgs.reason);
        if (eventArgs.errorDetails != nil) {
            NSLog(@"Error details: %@", eventArgs.errorDetails);
        }
    }];

}
- (void)startRecording{ 
	    [self.recognizer startContinuousRecognition];
}
- (void)stopRecording{ 
	[self.recognizer stopContinuousRecognition];
}
// 
// 数据处理回调函数
void HandleInputBuffer(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime, UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc) {
		// 在回掉函数中,将数据传递给微软,上面的回掉函数中就能拿到数据了
	   [self.audioInputStream write:audioData];
}

保存文件

  • 保存文件相对简单,录音开始清除上一次的音频文件,创建新的音频文件
  • WAV文件需要添加头文件,才能正常播放

#import "SaveAudioFile.h"


#define isValidString(string)               (string && [string isEqualToString:@""] == NO)
// WAV 文件头结构
typedef struct {
    char riff[4];
    UInt32 fileSize;
    char wave[4];
    char fmt[4];
    UInt32 fmtSize;
    UInt16 formatTag;
    UInt16 channels;
    UInt32 samplesPerSec;
    UInt32 avgBytesPerSec;
    UInt16 blockAlign;
    UInt16 bitsPerSample;
    char data[4];
    UInt32 dataSize;
} WAVHeader;

@implementation SaveAudioFile
/**
 * 清理文件
 */
- (void)cleanFile {
    
    if (isValidString(self.mp3Path)) {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL isDir = FALSE;
        BOOL isDirExist = [fileManager fileExistsAtPath:self.mp3Path isDirectory:&isDir];
        if (isDirExist) {
            [fileManager removeItemAtPath:self.mp3Path error:nil];
            NSLog(@"  xxx.mp3  file   already delete");
        }
    }
    if (isValidString(self.wavPath)) {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL isDir = FALSE;
        BOOL isDirExist = [fileManager fileExistsAtPath:self.wavPath isDirectory:&isDir];
        if (isDirExist) {
            [fileManager removeItemAtPath:self.wavPath error:nil];
            NSLog(@"  xxx.caf  file   already delete");
        }
    }
}
/**
 *  取得录音文件保存路径
 *
 *  @return 录音文件路径
 */
-(NSURL *)getSavePath{
    //  在Documents目录下创建一个名为FileData的文件夹
    NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)lastObject] stringByAppendingPathComponent:@"AudioData"];
    NSLog(@"%@",path);
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL isDir = FALSE;
    BOOL isDirExist = [fileManager fileExistsAtPath:path isDirectory:&isDir];
    if(!(isDirExist && isDir))
        
    {
        BOOL bCreateDir = [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
        if(!bCreateDir){
            NSLog(@"创建文件夹失败!");
        }
        NSLog(@"创建文件夹成功,文件路径%@",path);
    }
    NSString *fileName = @"record";
    NSString *wavFileName = [NSString stringWithFormat:@"%@.wav", fileName];
    NSString *mp3FileName = [NSString stringWithFormat:@"%@.mp3", fileName];
    
    NSString *wavPath = [path stringByAppendingPathComponent:wavFileName];
    NSString *mp3Path = [path stringByAppendingPathComponent:mp3FileName];
    
    self.wavPath = wavPath;
    self.mp3Path = mp3Path;
    
    NSLog(@"file path:%@",mp3Path);
    
    NSURL *url=[NSURL fileURLWithPath:mp3Path];
    return url;
}

-(void) startWritingHeaders {
    [self cleanFile];
    [self getSavePath];
    // 写入 WAV 头部
   WAVHeader header;
   memcpy(header.riff, "RIFF", 4);
   header.fileSize = 0;  // 将在录音结束时填充
   memcpy(header.wave, "WAVE", 4);
   memcpy(header.fmt, "fmt ", 4);
   header.fmtSize = 16;
   header.formatTag = 1;  // PCM
   header.channels = 1;
   header.samplesPerSec = 16000;
   header.avgBytesPerSec = 16000 * 2;
   header.blockAlign = 2;
   header.bitsPerSample = 16;
   memcpy(header.data, "data", 4);
   header.dataSize = 0;  // 将在录音结束时填充
    
    // 创建 WAV 文件API
   [[NSFileManager defaultManager] createFileAtPath:self.wavPath contents:nil attributes:nil];
   self.audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:self.wavPath];
    [self.audioFileHandle writeData:[NSData dataWithBytes:&header length:sizeof(header)]];
    // 创建 mp3 文件API
    [[NSFileManager defaultManager] createFileAtPath:self.mp3Path contents:nil attributes:nil];
    self.audioFileHandle2 = [NSFileHandle fileHandleForWritingAtPath:self.mp3Path];
}
- (void) saveAudioFile: (NSData *) data type:(NSString *) type{
    if([type isEqualToString:@"wav"]){
        // 写入音频数据到 WAV 文件
        [self.audioFileHandle writeData:data];
    }else{
        // 拿到编码过后的数据,保存到本地
        [self.audioFileHandle2 writeData:data];
    }
}
@end

利用Lame库编码PCM数据

  • 下载Lame库并导入项目中操作,参考网上文章https://www.cnblogs.com/XYQ-208910/p/7650759.html
  • lame库的使用主要分成3部分
    • 初始化Lame 并设置比特率,位深,通道数,压缩程度
    • 传入原始的音频数据,得到编码过后的mp3音频数据
    • 结束时刷新lame中还剩的数据,关闭Lame
//
//  LameEncoderMp3.m
//  SpeechUntil
//
//  Created by 肖鹏程 on 2024/7/25.
//

#import "LameEncoderMp3.h"


@implementation LameEncoderMp3

- (void) settingFormat:(int)sampleRate channels:(int)channels{
    // 初始化lame编码器 设置格式
     self.lame = lame_init();
    lame_set_in_samplerate(self.lame, sampleRate);
    lame_set_num_channels(self.lame, channels);
    lame_set_brate(self.lame, 16); // 比特率128 kbps
    lame_set_mode(self.lame, channels == 1 ? MONO : STEREO);
    lame_set_quality(self.lame, 7); // 0 = 最高质量(最慢),9 = 最低质量(最快)
    lame_init_params(self.lame);
    self.channels = channels;

};
- (NSData *)encodePCMToMP3:(NSData *)pcmData{

    // PCM数据的指针和长度
    const short *pcmBuffer = (const short *)[pcmData bytes];
    int pcmLength = (int)[pcmData length] / sizeof(short);
    NSLog(@"pcmLength %lu", [pcmData length]);
    // 分配MP3缓冲区
    int mp3BufferSize = (int)(1.25 * pcmLength) + 7200;
    unsigned char *mp3Buffer = (unsigned char *)malloc(mp3BufferSize);
    // 确保mp3Buffer分配成功
       if (mp3Buffer == NULL) {
           NSLog(@"Failed to allocate memory for MP3 buffer");
           return nil;
       }
    // PCM编码为MP3
    // 注意这个是单通道的方法,如果是双通道调用这个lame_encode_buffer_interleaved(
    //   lame,
    //   recordingData,
    //   numSamples / 2,  // 双声道
    //   mp3Buffer,
    //   mp3BufferSize
    );
    int mp3Length = lame_encode_buffer(self.lame, (short *)pcmBuffer, (short *)pcmBuffer,pcmLength, mp3Buffer, mp3BufferSize);
    if (mp3Length < 0) {
        NSLog(@"LAME encoding error: %d", mp3Length);
        free(mp3Buffer);
        return nil;
    }
    // 创建MP3数据
    NSData *mp3Data = [NSData dataWithBytes:mp3Buffer length:mp3Length];
    NSLog(@"mp3Length %lu", [mp3Data length]);
    // 清理
    free(mp3Buffer);
    
    return mp3Data;
}

- (NSData *) closeLame{
    // 刷新LAME缓冲区
  unsigned char mp3Buffer[7200];
  int flushLength = lame_encode_flush(self.lame, mp3Buffer, sizeof(mp3Buffer));
    NSData *flushData;
  if (flushLength > 0) {
      // 将刷新后的数据追加到已有的MP3数据
      flushData = [NSData dataWithBytes:mp3Buffer length:flushLength];
  } else if (flushLength < 0) {
      NSLog(@"LAME flushing error: %d", flushLength);
  }
  // 关闭
    lame_close(self.lame);
    return flushData;
}

@end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1956542.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

监控服务器状态,夜莺( nightingale)方案介绍

前提 选择一台服务器&#xff0c;部署一个监控服务端&#xff0c;然后在各个windows或者linux中部署数据采集节点。 类似如下结构 目前我这边采用的技术是 https://flashcat.cloud/product/nightingale/ Nightingale 的解决方案 1 部署内容 服务端&#xff08;服务端为 c…

centos7安装思源黑体

centos7安装思源黑体 下载思源字体centos7查看目前字体查看所有字体查看所有中文字体 进入字体目录将新加的字体目录添加到fonts.conf配置文件刷新字体缓存查看效果 下载思源字体 下载地址: https://github.com/adobe-fonts/source-han-sans/releases centos7查看目前字体 查…

【MyBatis】基础操作

准备工作 准备数据库表创建 springboot工程&#xff0c;选择引入对应的起步依赖&#xff08;mybatis、mysql驱动、lombok&#xff09;application.properties中引入数据库连接信息创建对应的实体类 Emp&#xff08;实体类属性采用驼峰命名&#xff09;准备Mapper接口 EmpMappe…

排序系列 之 希尔排序

&#xff01;&#xff01;&#xff01;排序仅针对于数组哦本次排序是按照升序来的哦 介绍 英文名为ShellSort&#xff0c;又称“缩小增量排序”是直接插入排序算法的一种更高效的改进版本希尔排序是把记录按下标的指定步长分组&#xff0c;然后按照每组使用直接插入排序&#…

idea一直update indexing 卡死

打开IDEA存储应用程序的本地数据文件夹&#xff0c;关闭IDEA&#xff0c;删除caches和index文件夹下的文件&#xff0c;重新打开。

数据加密技术在数据安全中起到什么样的作用?

把数据以及一个密钥&#xff0c;通过相关的加密算法&#xff0c;进行一系列的加密算法计算处理&#xff0c;使这个数据变成密文&#xff0c;保护数据的机密性。数据加密技术是一种将原始数据&#xff08;明文&#xff09;通过算法转换成只有授权用户才能解读的格式&#xff08;…

C++客户端Qt开发——系统相关(多媒体音频)

3.多媒体&#xff08;音频、视频&#xff09; 播放声音需要引入multimedia模块 使用QSound类 仅支持的音频文件格式为.wav&#xff0c;同样使用qrc文件管理外部的资源 &#xff08;使用的.wav文件不宜过大&#xff0c;尽量在几秒内&#xff0c;否则会构建时间过长&#xff…

拓维思树障分析Tovos PowerLine 4.0.19 航线规划Tovos SmartPlan 2.0.0 下载License电力应用软件使用

Tovos PowerLine 是功能强大的输电线路智能巡检系统&#xff01;这是一个专业且智能的软件&#xff0c;能够更准确的进行巡检和对线路设备进行精确的测量&#xff0c;通过获取高精度的点云来获取精准的三维路线的地形地貌、设备设施、途径的各种物体等来精确您的三维空间信息和…

三子棋小程序

一.自定义头文件(game.h) 放入源文件需要用到的标准库头文件和函数的声明 ROW 和COL为棋盘的行和列&#xff0c;三子棋嘛&#xff0c;肯定为3啦 #pragma once #include<stdio.h> #include<String.h> #include<stdlib.h> #include<time.h> #define ROW…

中国少女徐可昕心怀爱豆梦 为实现梦想勇赴韩国

如今MZ世代(MZ世代是‌Millennial世代和‌Z世代的合成词,用来描述1980至2000年间出生的人)小伙伴们追求着怎样的梦想呢?随着韩国K-POP热潮和娱乐产业的发展,越来越多的年轻人希望可以在娱乐圈工作,例如成为爱豆或经理人。通过抖音或YouTube等,可以发现很多才华横溢的年轻人创作…

malloc 对比验证 posix_memalign 的功能

1&#xff0c;函数说明 posix_memalign 是一个 POSIX 标准定义的函数&#xff0c;用于在内存中分配对齐的内存块。这个函数可以确保分配的内存块满足特定的对齐要求&#xff0c;通常用于需要对齐内存访问的情况&#xff0c;比如 SIMD 指令集的使用或者特定硬件要求的内存对齐。…

瑞派明星医生:技术深耕教学指导,燃动当代名校兽医双效技能Buff

名校精进&#xff0c;攻破技术堡垒 从动物保护、环境保护的最初理想&#xff0c;到兽医学专业领域深耕&#xff0c;天津瑞派长江宠物医院的唐玉洁医生正用自己的实际行动展现着当代兽医人才背后的奉献、良知、超越与传承。 享受了东北林业大学带来的短暂而有趣的大学生活&…

聊聊RNN与Attention

前言 Attention Mechanism&#xff0c;称为注意力机制。基于Attention机制&#xff0c;seq2seq可以像我们人类一样&#xff0c;将“注意力”集中在必要的信息上。 Attention的结构 seq2seq存在的问题 seq2seq中使用编码器对时序数据进行编码&#xff0c;然后将编码信息传递…

JS:JavaScript 简介

前言 在当今数字时代&#xff0c;JavaScript已然成为了现代Web开发的基石。从最初作为浏览器中的一个小型脚本语言&#xff0c;到如今成为驱动全球互联网的强大引擎&#xff0c;JavaScript的发展历程本身就是一个令人着迷的故事。 好了开始学习JS吧 1. 如果刚开始了解js&#…

Node.js自动化处理TOML文件

在软件开发过程中&#xff0c;自动化处理配置文件是一种常见的需求。TOML&#xff08;Tom’s Obvious, Minimal Language&#xff09;是一种用于配置文件的简单易读的格式。本文将展示如何使用Node.js和一些流行的库来自动化读取、修改并写入TOML文件。 1. 准备工作 在开始之前…

IAR工程设置

这篇记录一下工作中对IARide的工程设置的配置步骤。 所以这不是一篇通用的文章&#xff0c;只是个人记录。 目的&#xff1a;为了是ARM仿真器能够下载调试锐能微的芯片 一、工程设置 1、更改boot文件的工程设置&#xff1a; 2、编译boot文件 3、配置8213B的工程设置&#x…

iPhone 16 Pro Max电池大揭秘,听说迎来了有史以来最持久的续航

智能手机市场的竞争日益激烈&#xff0c;各大厂商都在寻求创新以保持领先地位。而苹果公司即将推出的iPhone 16 Pro Max&#xff0c;则以其革命性的不锈钢电池壳和电诱导粘合剂脱离技术&#xff0c;再次刷新了我们对智能手机电池技术的认知。 一、不锈钢电池壳的突破 苹果公司…

Windows系统编程API 学习之路

目录 I. 基础 进程 动态链接库 线程 Windows应用程序开发 初入Windows编程 使用strings C/C运行时中的字符串 安全字符串函数 32位与64位开发 定义自定义错误代码 I. 基础 Windows NT操作系统系列有着悠久的历史&#xff0c;从1993年推出的3.1版本开始。今天的Windo…

如何主持收集故事研讨会

如何主持收集故事研讨会 引言 在文化的长河中,故事是传承知识、情感与智慧的桥梁。收集故事不仅是对过去的回顾,更是对未来灵感的启迪。组织一场收集故事研讨会,旨在汇聚各界人士,共同挖掘、整理并分享那些触动人心、富有意义的故事。作为这样一场活动的主持人,您的角色…

C++开源界面库duilib的使用细节与实战技巧总结(实战经验分享)

目录 1、使用CEditUI编辑框 2、使用CLabelUI或CTextUI的Html文本效果 3、使用CTextUI控件对文字宽度自适应的特性 4、CRichEditUI富文本控件使用注意点 4.1、指定CRichEditUI加在2.0版本的Riched20.dll库 4.2、解决向CRichEditUI中插入文字后显示空白的问题 5、设置窗口…