光滑的UIButton

本文通过逐步指导的方式,教你如何使用CoreGraphics从零开始创建一个带有高光效果、阴影及渐变色的圆角按钮。

从零开始DIY酷炫按钮!Create your own cool and flexible buttons from scratch!

在教程1,2和3,我们讲解了如何个性化定制一个Table view的全过程 – 仅仅使用了Core Graphics!

在这篇教程中,我们将着手另一个实例 – 如何个性化定制一个UIButton。

在教程中,我们将会学到如何绘制圆角矩形,如何对使用Core Graphics绘制的图形进行简单的染色处理,并且巩固我们之前学过的一些概念。

正如来自Under The BridgeAlex Curylo多次提到过的,已经有很多关于如何个性化定制UIButton的方法了。

我个人推荐的快速简易制作按钮的方法是Dermot Daly写的Button Maker。但是我认为在这种讨论中缺少的是如何使用Core Graphics个性化定制你的按钮的详细教程。

教程相当的简单,用这种教学方式,你可以直观的了解你将要开发的app内容。现在让我们开始制作按钮吧!开始

在Xcode中,使用View-based Application 模版创建一个新的项目,命名为“CoolButton”。

然后确保你选中了 “Groups & Files”中的”Classes” 组,进入菜单的”FileNew File…”,选择 iOSCocoa Touch Class,Objective-C 类,确保”Subclass of UIView”被选中,然后点击下一步。命名文件为 “CoolButton.m”,然后注意选上”Also create CoolButton.h”,然后点击”完成“。

接着使用以下的代码,替换掉CoolButton.h文件的内容:

#import <UIKit/UIKit.h>
@interface CoolButton : UIButton {
    CGFloat _hue;
    CGFloat _saturation;
    CGFloat _brightness;
}
@property  CGFloat hue;
@property  CGFloat saturation;
@property  CGFloat brightness;
@end

注意到我们没有继承UIVIew,而是继承UIButton。我们还定义了一些属性变量,去设置view的色彩,饱和度还有亮度。

接下来,我们使用以下内容对CoolButton.m文件做改动:

// Under @implementation
@synthesize hue = _hue;
@synthesize saturation = _saturation;
@synthesize brightness = _brightness;
// Delete initWithFrame and add the following:
-(id) initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        self.opaque = NO;
        self.backgroundColor = [UIColor clearColor];
        _hue = 1.0;
        _saturation = 1.0;
        _brightness = 1.0;
    }
    return self;
}
// Uncomment drawRect and replace the contents with:
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef color = [UIColor colorWithHue:_hue saturation:_saturation 
    brightness:_brightness alpha:1.0].CGColor;
 
CGContextSetFillColorWithColor(context, color);
CGContextFillRect(context, self.bounds);
// Add to the bottom of the file
- (void)setHue:(CGFloat)hue {
    _hue = hue;
    [self setNeedsDisplay];
}
- (void)setSaturation:(CGFloat)saturation {
    _saturation = saturation;
    [self setNeedsDisplay];
}
- (void)setBrightness:(CGFloat)brightness {
    _brightness = brightness;
    [self setNeedsDisplay];
}

这里我们只是初始化了一些变量,使用设定的颜色去填充整个按钮,确保一切正常开始。

注意到我们使用了不同的构造函数去设置颜色 – 而不是使用 colorWithRed:Green:Blue方法,我们使用了colorWithHue:saturation:brightness方法。这将让接下来的操作更简单。

我们最后要做的事情,是重写hue(颜色),saturation(饱和度)和brightness(亮度)属性变量的setter方法,然后当setter方法被调用的时候,我们调用setNeedsDisplay方法。这会在用户改变按钮的颜色的时候,强制我们的view去重绘一遍。

现在,我们回到CoolButtonViewController.h文件,使用以下代码对它进行修改:

// Before @interface
@class CoolButton;
// Inside @interface
CoolButton *_button;
// After @interface
@property (retain) IBOutlet CoolButton *button;
- (IBAction)hueValueChanged:(id)sender;
- (IBAction)saturationValueChanged:(id)sender;
- (IBAction)brightnessValueChanged:(id)sender;

 

这里我们申明了一个对button的outlet引用(我们将在Interface Builder中添加)和一些设定slider控件属性值时的Action回调函数(我们也将会在Interface Builder中添加)。

现在让我们继续吧。打开CoolButtonViewController.m文件,然后拖动一个UIButton,3个UILabel和3个UISlider控件到view中,像下图一样:
Button View Layout

接下来,转到Identity Inspector,修改Class的下拉选项为CoolButton,从而改变UIButton的class属性为CoolButton。

Identity Inspector

另外,确保Attributes Inspector被选中,然后切换Button的drawing type为Custom,用以取消按钮圆角的默认设定。

然后,control键+鼠标拖动“File’s Owner”到CoolButton,然后连接到按钮的outlet上。相似的,control键+鼠标拖动slider的“File’s Owner”,连接到恰当的数值变化回调函数。在我们运行代码之前,使用以下代码修改CustomViewController.m文件:

// In the import section
#import "CoolButton.h"
// Under @implementation
@synthesize button = _button;
// In viewDidUnload
self.button = nil;
// In dealloc
[_button release];
_button = nil;
// Add to the bottom of the file
- (IBAction)hueValueChanged:(id)sender {
 
    UISlider *slider = (UISlider *)sender;
    _button.hue = slider.value;
 
}
- (IBAction)saturationValueChanged:(id)sender {
 
    UISlider *slider = (UISlider *)sender;
    _button.saturation = slider.value;
 
}
- (IBAction)brightnessValueChanged:(id)sender {
 
    UISlider *slider = (UISlider *)sender;
    _button.brightness = slider.value;
 
}

注意到UISlider的默认值范围设定为0.0到1.0, 和我们变化范围在0.0到1.0的颜色值,饱和度和亮点值一致,所以我们可以直接对它们进行设定了。编译运行工程,然后如果一切都运行正常,你应该能使用slider去调整”CoolButton”的颜色。

Button Placeholder

绘制圆角矩形按钮

你其实也可以使用直角按钮,但是目前大部分的软件都是用圆角按钮的。

在最后的教程中,我们讲解了如何使用CGContextAddArc API去绘制弧线。基于经验我们当然可以使用它在每个边角处绘制弧线了,然后绘制线条去连接他们。
但是有个更简单的方法,让我们不需要做这么多数学运算,它很适合绘制圆角矩形。这种方法就是CGContextAddArcToPoint API。

CGContextAddArcToPoint API可以让你在绘制弧线的时候,指定两条切线和半径的大小。下面来自Quartz2D Programming Guide的示意图很好的解释了相关含义。
CGContextAddArcToPoint Diagram

所以对于一个矩形的情况,我们很明显知道每条想要绘制的弧线的切线 – 他们就是矩形的边!并且我们可以根据想要的矩形角弧度去指定半径的大小 – 更大,更圆。

另外还需要对这个函数补充的是如果当前在轨迹上的点没有设定到你开始绘制弧线的位置,程序就会从当前点到轨迹的初始位置绘制一条线。所以你可以使用这种简便方法,仅调用几个函数去绘制一个圆角矩形。

因为我们将要在这篇教程中去创建多个圆角矩形,让我们在Common.h/m文件中添加一个辅助函数去创建圆角矩形的轨迹方法,获得一个矩形。

如果你现在还没有Common.h/m文件,请到这里下载Common.h/m,并且添加到你的工程中。然后将以下内容添加到Common.h文件中:

CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius);

添加以下代码到Common.m文件的底部:

CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius) {
 
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMinY(rect));
    CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMinY(rect), 
        CGRectGetMaxX(rect), CGRectGetMaxY(rect), radius);
    CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMaxY(rect), 
        CGRectGetMinX(rect), CGRectGetMaxY(rect), radius);
    CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMaxY(rect), 
        CGRectGetMinX(rect), CGRectGetMinY(rect), radius);
    CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect), 
        CGRectGetMaxX(rect), CGRectGetMinY(rect), radius);
    CGPathCloseSubpath(path);
 
    return path;        
}

上面的代码按照以下图示顺序绘制了圆角矩形:

How to draw a rounded rect with Core Graphics

  1. 移动到顶部线段的中心点。
  2. 为右上角添加一段弧线。在绘制弧线之前, CGPathAddArcToPoint函数会从当前点(矩形的中间)到弧形的初始位置绘制一条线。
  3. 类似的,为右下脚和连接线添加一段弧线。
  4. 类似的,为左下脚和连接线添加一段弧线。
  5. 类似的,为左上脚和连接线添加一段弧线。
  6. 然后使用CGPathCloseSubpath函数,连接弧线的结束点和开始点。

另外注意到上面,我们使用了一些来自CGGeometry.h文件的辅助函数,在我们提供的矩形中,来获得多个位置点信息。好的,让我们现在来看下它的原理!打开 CoolButton.m文件,根据以下代码做修改:

// At top of file
#import "Common.h"
// Replace the contents of drawRect with the following:
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:_brightness alpha:1.0].CGColor;
CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 
        blue:0.2 alpha:0.5].CGColor;
CGFloat outerMargin = 5.0f;
CGRect outerRect = CGRectInset(self.bounds, outerMargin, outerMargin);
CGMutablePathRef outerPath = createRoundedRectForRect(outerRect, 6.0);
if (self.state != UIControlStateHighlighted) {
	CGContextSaveGState(context);
	CGContextSetFillColorWithColor(context, outerTop);
	CGContextSetShadowWithColor(context, CGSizeMake(0, 2), 3.0, shadowColor);
	CGContextAddPath(context, outerPath);
	CGContextFillPath(context);
	CGContextRestoreGState(context);
}

这里我们定义了自己的两种颜色,然后使用了CGRectInset函数去获取一个更小的矩形(每条边有5个像素点),我们将会在这个矩形上绘制圆角矩形。并且我们把它做的尽量小以便留有空间在它的外围绘制阴影。

接下来,我们调用刚才写好的函数为我们的圆角矩形创建轨迹,然后设置填充颜色和阴影,添加轨迹到我们的context上面,并且调用FillPath函数,使用当前颜色填充整个矩形。

注意我们只希望在按钮没有被点击的时候,运行代码。编译并运行app,如果一些运行正常,你将看到以下画面:
Rounded Rect, Draft 2

美化我们的按钮

我们现在有个像按钮模样的东西了!但是还不够美观:[

让我们现在着手处理下吧!增加一些重要的改进步骤。打开CoolButton.m文件,并根据以下代码做修改:

// Replace the colors section with the following
CGColorRef blackColor = [UIColor colorWithRed:0.0 green:0.0 
        blue:0.0 alpha:1.0].CGColor;
CGColorRef highlightStart = [UIColor colorWithRed:1.0 green:1.0 
        blue:1.0 alpha:0.4].CGColor;
CGColorRef highlightStop = [UIColor colorWithRed:1.0 green:1.0 
        blue:1.0 alpha:0.1].CGColor;
CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 
        blue:0.2 alpha:0.5].CGColor;
CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:1.0*_brightness alpha:1.0].CGColor;
CGColorRef outerBottom = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.80*_brightness alpha:1.0].CGColor;
CGColorRef innerStroke = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.80*_brightness alpha:1.0].CGColor;
CGColorRef innerTop = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.90*_brightness alpha:1.0].CGColor;
CGColorRef innerBottom = [UIColor colorWithHue:_hue saturation:_saturation 
        brightness:0.70*_brightness alpha:1.0].CGColor;
// Add the following to the bottom
CGContextSaveGState(context);
CGContextAddPath(context, outerPath);
CGContextClip(context);
drawGlossAndGradient(context, outerRect, outerTop, outerBottom);
CGContextRestoreGState(context);

首先我们设置了多种颜色,以便后面使用。有一些普通的颜色,还有一些基于以前的参数设置的颜色。基本颜色是我们构建的,还构建了其他一些颜色,让它们比当前颜色在不同程度上更暗些。

如果你这样去定义颜色,可以让你更容易去改变view(视图)的颜色,提高代码的可重用性!

接下来我们要做的就是裁切我们的圆角矩形,然后用渐变的颜色去填充它(而不是单一的颜色)。编译并运行,我们的按钮现在变得更美观了:

Button Styling - Update 1

现在让我们在按钮内部轨迹添加一种跟外部不同的渐变颜色,创建一种斜面视觉效果。使用以下代码对CoolButton.m文件进行修改:

// Add after the creation of the outerPath
CGFloat innerMargin = 3.0f;
CGRect innerRect = CGRectInset(outerRect, innerMargin, innerMargin);
CGMutablePathRef innerPath = createRoundedRectForRect(innerRect, 6.0);
// At the bottom
CGContextSaveGState(context);
CGContextAddPath(context, innerPath);
CGContextClip(context);
drawGlossAndGradient(context, innerRect, innerTop, innerBottom);
CGContextRestoreGState(context);

这里我们使用CGRectInset函数进一步缩小矩形,然后利用它获得一个圆角矩形,并且填充上渐变颜色。编译运行,你会看到一些微妙的改进:

Button Styling - Update 2

当按钮被点击的时候,我们在它的顶部添加一点高亮效果。使用以下代码对CoolButton.m文件做进一步的修改:

// After the creation of the innerPath
CGFloat highlightMargin = 2.0f;
CGRect highlightRect = CGRectInset(outerRect, highlightMargin, highlightMargin);
CGMutablePathRef highlightPath = createRoundedRectForRect(highlightRect, 6.0);
// At the bottom
if (self.state != UIControlStateHighlighted) {
    CGContextSaveGState(context);
    CGContextSetLineWidth(context, 4.0);
    CGContextAddPath(context, outerPath);
    CGContextAddPath(context, highlightPath);
    CGContextEOClip(context);
    drawLinearGradient(context, outerRect, highlightStart, highlightStop);
    CGContextRestoreGState(context);
}

我们在创建另一个比外部矩形稍微小的圆角矩形,并且在两个矩形之间使用一种alpha高亮渐变颜色去填充,使用我们上篇教程提到的Even-Odd Clip技巧。编译运行程序,你会发现这是很微妙的变化:
Button Styling - Update 3

让我们结束所有的步骤吧。添加以下代码到drawRect:方法的底部:

CGContextSaveGState(context);
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, blackColor);
CGContextAddPath(context, outerPath);
CGContextStrokePath(context);
CGContextRestoreGState(context);
CGContextSaveGState(context);
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, innerStroke);
CGContextAddPath(context, innerPath);
CGContextClip(context);
CGContextAddPath(context, innerPath);
CGContextStrokePath(context);
CGContextRestoreGState(context);    
CFRelease(outerPath);
CFRelease(innerPath);
CFRelease(highlightPath);

我们这里所做的是对外部轨迹使用黑色上色(使用两个像素点以避免1px的问题),然后使用1像素点对内部轨迹上色。

你可能会感慨,”我的天“,“那是两个像素点,而不是一个!”好的,我们这里使用了不同的技术解决ipx的问题 – 之前提到过的”clipping mask”技巧。基本上我们设定笔画为2个像素点,然后裁剪掉外部的区域,最后我们释放掉创建的轨迹线。

编译运行app,看下我们的按钮吧!
Button Styling - Update 4

几笔画能做到的这样子确实挺赞的!

Highlighting the Button

突出显示按钮

虽然 我们的按钮看起来相当酷,但是它并不像一个按钮那样可以响应点击操作。没有任何迹象可以看出按钮是否被按下。

幸运的是,Jeff LaMarche在他的文章 posts on the subject 中展示了解决方案。

基本的思路是我们需要重写touch events的方法,去让按钮自己重新绘制显示,因为它需要根据点击状态去更新外形。使用下面的代码对CoolButton.m文件进行修改:

// Add the following methods to the bottom
- (void)hesitateUpdate
{
    [self setNeedsDisplay];
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    [self setNeedsDisplay];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    [self setNeedsDisplay];
 
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    [self setNeedsDisplay];
    [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    [self setNeedsDisplay];
    [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1];
}
编译运行工程,现在当你点击按钮的时候,你将看到不同的变化 - 突出显示和点击消失了。现在让我们修改一下代码做得更好一些:
// At beginning of function
CGFloat actualBrightness = _brightness;
if (self.state == UIControlStateHighlighted) {
    actualBrightness -= 0.10;
}   
// Then replace all further cases in the function that use 
//      "_brightness" to define colors with "actualBrightness".

重新编译运行,现在当你点击按钮的时候,按钮看起来更美观了!

Tapped Button

Where To Go From Here?

接下来我们可以做什么?

本教程的例子源代码工程,可以到这里下载

既然我们从零开始了解了如果创建个性化按钮的全过程,你将对如何个性化定制按钮,以便使用到自己的工程中觉得相当熟悉了!

接下来在Core Graphics 101 教程系列的最后一篇教程,我们将讨论如何使用模式去制作很酷的效果!


classdef HeatEquationSolver < matlab.apps.AppBase % 属性 properties (Access = public) UIFigure matlab.ui.Figure TabGroup matlab.ui.container.TabGroup % 设置选项卡组件 SettingsTab matlab.ui.container.Tab SolveButton matlab.ui.control.Button StatusLabel matlab.ui.control.Label % 空间区域设置 DomainLabel matlab.ui.control.Label LLabel matlab.ui.control.Label LEditField matlab.ui.control.NumericEditField NLabel matlab.ui.control.Label NEditField matlab.ui.control.NumericEditField % 时间设置 TimeLabel matlab.ui.control.Label TfinalLabel matlab.ui.control.Label TfinalEditField matlab.ui.control.NumericEditField dtLabel matlab.ui.control.Label dtEditField matlab.ui.control.NumericEditField % 物理参数设置 PhysicalParamsLabel matlab.ui.control.Label AlphaLabel matlab.ui.control.Label AlphaEditField matlab.ui.control.NumericEditField % 初始条件和边界条件设置 ICLabel matlab.ui.control.Label ICDropDown matlab.ui.control.DropDown ICPreviewPanel matlab.ui.container.Panel ICPreviewAxes % 修改为通用类型,兼容axes和uiaxes BCLabel matlab.ui.control.Label BCDropDown matlab.ui.control.DropDown % 数值方法设置 MethodLabel matlab.ui.control.Label SpectralMethodDropDown matlab.ui.control.DropDown CompareWithFDMCheckBox matlab.ui.control.CheckBox % 可视化选项卡组件 VisualizationTab matlab.ui.container.Tab SolutionPlot % 修改为通用类型,兼容axes和uiaxes TimeSlider matlab.ui.control.Slider CurrentTimeLabel matlab.ui.control.Label PlayButton matlab.ui.control.Button AnimationSpeedLabel matlab.ui.control.Label AnimationSpeedDropDown matlab.ui.control.DropDown % 误差分析选项卡组件 ErrorAnalysisTab matlab.ui.container.Tab ErrorPlot % 修改为通用类型,兼容axes和uiaxes PerformanceMetricsTextArea matlab.ui.control.TextArea PerfBarAxes % 修改为通用类型,兼容axes和uiaxes % 帮助选项卡 HelpTab matlab.ui.container.Tab end % 内部属性 - 存储计算结果 properties (Access = private) x % 空间网格点 t % 时间点 u_spectral % 谱方法解 u_fdm % 有限差分法解(如果选择比较) u_exact % 精确解(如果有) errorData % 误差数据 solveTime % 求解时间 fdmSolveTime % 有限差分法求解时间 AnimationTimer % 动画定时器 % 计算参数 L % 计算域长度 N % 空间离散点数 Tfinal % 最终时间 dt % 时间步长 alpha % 热扩散系数 % 状态标志 isSolved % 是否已求解 end methods (Access = private) % 初始条件函数 function u0 = getInitialCondition(app, x) switch app.ICDropDown.Value case '高斯脉冲' % 高斯脉冲 %单个高斯脉冲重复排列,形成周期性的信号 %将脉冲置于计算域的中心,可以保证在周期性边界条件下,脉冲的左右部分对称,避免因边界不连续导致的数值误差。 %L/20意味着脉冲的宽度相对于整个计算域L来说较小,但又不至于太小导致数值离散化困难。 %在显式时间积分方法中,时间步长dt通常受CFL(Courant-Friedrichs-Lewy)条件限制。 % 较小的σ可能导致空间梯度更大,需要更小的时间步长来保持稳定性。选择σ=L/20可能在保证一定精度的同时,允许使用较大的dt,提高计算效率。 sigma = app.L/20; u0 = exp(-(x-app.L/2).^2/(2*sigma^2)); case '阶跃函数' % 阶跃函数 u0 = zeros(size(x)); u0(x >= app.L/4 & x <= 3*app.L/4) = 1; % case '正弦函数' % 正弦函数 u0 = sin(pi*x/app.L); case '自定义' % 这里可以添加用户输入自定义初始条件的功能 u0 = sin(pi*x/app.L); % 默认为正弦函数 end %高斯脉冲:在计算域中心 (L/2) 生成高斯分布,标准差 σ = L/20。 %阶跃函数:在区间 [L/4, 3L/4] 内设为1,其他区域为0。 %正弦函数:生成波长与计算域长度匹配的正弦波,生成正弦波:`u0 = sin(πx/L)`,波长为`2L`。 end % 更新初始条件预览图 function updateICPreview(app) % 获取当前计算域参数 L = app.LEditField.Value; N = 100; % 预览用的固定点数 % 创建均匀网格 dx = L/N; x = (0:N-1)'*dx; % 临时设置app.L以便于getInitialCondition使用 app.L = L; % 获取初始条件 u0 = app.getInitialCondition(x); % 绘制初始条件预览 cla(app.ICPreviewAxes); plot(app.ICPreviewAxes, x, u0, 'b-', 'LineWidth', 1.5); xlabel(app.ICPreviewAxes, 'x'); ylabel(app.ICPreviewAxes, 'u₀'); title(app.ICPreviewAxes, '初始条件'); grid(app.ICPreviewAxes, 'on'); app.ICPreviewAxes.Box = 'on'; end % 使用傅里叶谱方法求解函数 function solve_fourier_spectral(app) tic; % 开始计时 % 获取计算参数 app.L = app.LEditField.Value; app.N = app.NEditField.Value; app.Tfinal = app.TfinalEditField.Value; app.dt = app.dtEditField.Value; app.alpha = app.AlphaEditField.Value; % 创建空间网格 dx = app.L/app.N; app.x = (0:app.N-1)'*dx; % 创建时间点 Nt = ceil(app.Tfinal/app.dt); app.dt = app.Tfinal/Nt; % 调整dt以匹配Tfinal app.t = (0:Nt)'*app.dt; % 初始条件 u0 = app.getInitialCondition(app.x); % 初始化解 app.u_spectral = zeros(app.N, Nt+1); app.u_spectral(:,1) = u0; % 计算波数 if mod(app.N, 2) == 0 k = 2*pi/app.L * [0:app.N/2-1, 0, -app.N/2+1:-1]'; else k = 2*pi/app.L * [0:(app.N-1)/2, -(app.N-1)/2:-1]'; end % 傅里叶变换初始条件 u_hat = fft(u0); % 时间推进 for n = 1:Nt % 热方程的谱解:du_hat/dt = -alpha*k^2*u_hat % 使用精确积分因子求解 u_hat = u_hat .* exp(-app.alpha*k.^2*app.dt); % 反变换回物理空间 app.u_spectral(:,n+1) = real(ifft(u_hat)); end % 记录求解时间 app.solveTime = toc; % 设置状态标志 app.isSolved = true; % 如果选择同时使用有限差分法,则计算FDM解 if app.CompareWithFDMCheckBox.Value app.solve_fdm(); end % 更新界面 app.updateVisualization(); app.updateErrorAnalysis(); end % 使用有限差分法求解函数 function solve_fdm(app) tic; % 开始计时 % 获取计算参数 app.L = app.LEditField.Value; app.N = app.NEditField.Value; app.Tfinal = app.TfinalEditField.Value; app.dt = app.dtEditField.Value; app.alpha = app.AlphaEditField.Value; % 创建均匀网格 dx = app.L/app.N; app.x = (0:app.N-1)'*dx; % 创建时间点 Nt = ceil(app.Tfinal/app.dt); app.dt = app.Tfinal/Nt; % 调整dt以匹配Tfinal app.t = (0:Nt)'*app.dt; % 初始条件 u0 = app.getInitialCondition(app.x); % 初始化解 if app.CompareWithFDMCheckBox.Value && strcmp(app.SpectralMethodDropDown.Value, '有限差分法') % 如果当前是FDM作为主方法,且需要比较,那么存储在u_fdm中 app.u_fdm = zeros(app.N, length(app.t)); app.u_fdm(:,1) = u0; % 并且调用傅里叶谱方法作为比较 app.solve_fourier_for_comparison(); % 最后将FDM结果复制到u_spectral以便统一显示 app.u_spectral = app.u_fdm; else % 如果是独立求解或者作为比较方法,那么存储在u_fdm中 app.u_spectral = zeros(app.N, length(app.t)); app.u_spectral(:,1) = u0; if app.CompareWithFDMCheckBox.Value % 如果是作为比较方法,也初始化u_fdm app.u_fdm = zeros(app.N, length(app.t)); app.u_fdm(:,1) = u0; end end % 计算稳定时间步长 dt_stable = 0.5*dx^2/app.alpha; % 检查时间步长是否稳定 dt_fdm = min(app.dt, dt_stable); r = app.alpha * dt_fdm / dx^2; % 扩散数 % 构造有限差分矩阵(使用中心差分) e = ones(app.N, 1); A = spdiags([e -2*e e], [-1 0 1], app.N, app.N); % 根据边界条件调整矩阵 switch app.BCDropDown.Value case '周期性边界' A(1, app.N) = 1; A(app.N, 1) = 1; case '狄利克雷边界' A(1, :) = 0; A(app.N, :) = 0; end % 显式时间推进(前向Euler) u_current = u0; % 确定要存储结果的数组 if app.CompareWithFDMCheckBox.Value && strcmp(app.SpectralMethodDropDown.Value, '傅里叶谱方法') % 如果作为比较方法,存储在u_fdm中 result_array = app.u_fdm; else % 否则存储在u_spectral中 result_array = app.u_spectral; end % 时间推进循环 for n = 1:length(app.t)-1 u_next = u_current + r * (A * u_current); % 应用边界条件 switch app.BCDropDown.Value case '狄利克雷边界' u_next(1) = 0; u_next(app.N) = 0; end if app.CompareWithFDMCheckBox.Value && strcmp(app.SpectralMethodDropDown.Value, '傅里叶谱方法') app.u_fdm(:,n+1) = u_next; else app.u_spectral(:,n+1) = u_next; end u_current = u_next; end % 记录求解时间 if app.CompareWithFDMCheckBox.Value && strcmp(app.SpectralMethodDropDown.Value, '傅里叶谱方法') % 如果作为比较方法,记录到fdmSolveTime app.fdmSolveTime = toc; else % 否则记录到主求解时间 app.solveTime = toc; % 设置状态标志 app.isSolved = true; end % 如果使用FDM作为主方法,且不需要比较,则不需要更新界面 % 因为调用者会负责更新 if ~(app.CompareWithFDMCheckBox.Value && strcmp(app.SpectralMethodDropDown.Value, '傅里叶谱方法')) % 更新界面 app.updateVisualization(); app.updateErrorAnalysis(); end end % 使用傅里叶谱方法作为比较方法 function solve_fourier_for_comparison(app) tic; % 开始计时 % 获取计算参数(共用主方法的参数) % 空间和时间网格已经在主方法中设置 % 初始条件 u0 = app.getInitialCondition(app.x); % 初始化解 app.u_spectral = zeros(app.N, length(app.t)); app.u_spectral(:,1) = u0; % 计算波数 if mod(app.N, 2) == 0 k = 2*pi/app.L * [0:app.N/2-1, 0, -app.N/2+1:-1]'; else k = 2*pi/app.L * [0:(app.N-1)/2, -(app.N-1)/2:-1]'; end % 傅里叶变换初始条件 u_hat = fft(u0); % 时间推进 for n = 1:length(app.t)-1 % 热方程的谱解:du_hat/dt = -alpha*k^2*u_hat % 使用精确积分因子求解 u_hat = u_hat .* exp(-app.alpha*k.^2*app.dt); % 反变换回物理空间 app.u_spectral(:,n+1) = real(ifft(u_hat)); end % 记录求解时间 app.solveTime = toc; end % 更新可视化函数 function updateVisualization(app) if ~app.isSolved return; end % 获取时间滑块的当前值 timeIndex = round(app.TimeSlider.Value); % 获取当前选择的方法 currentMethod = app.SpectralMethodDropDown.Value; % 绘制解 cla(app.SolutionPlot); hold(app.SolutionPlot, 'on'); % 绘制主方法解 plot(app.SolutionPlot, app.x, app.u_spectral(:,timeIndex), 'b-', 'LineWidth', 2); % 如果有比较方法,也绘制 if app.CompareWithFDMCheckBox.Value plot(app.SolutionPlot, app.x, app.u_fdm(:,timeIndex), 'r--', 'LineWidth', 1.5); if strcmp(currentMethod, '傅里叶谱方法') legend(app.SolutionPlot, '傅里叶谱方法', '有限差分法'); else legend(app.SolutionPlot, '有限差分法', '傅里叶谱方法'); end else legend(app.SolutionPlot, currentMethod); end % 设置标题和轴标签 title(app.SolutionPlot, sprintf('t = %.4f', app.t(timeIndex))); xlabel(app.SolutionPlot, 'x'); ylabel(app.SolutionPlot, 'u(x,t)'); grid(app.SolutionPlot, 'on'); % 更新当前时间标签 app.CurrentTimeLabel.Text = sprintf('当前时间: %.4f / %.4f', app.t(timeIndex), app.Tfinal); hold(app.SolutionPlot, 'off'); end % 更新误差分析函数 function updateErrorAnalysis(app) if ~app.isSolved || ~app.CompareWithFDMCheckBox.Value return; end % 获取当前选择的方法 currentMethod = app.SpectralMethodDropDown.Value; % 计算两种方法的L2误差 error = zeros(size(app.t)); for i = 1:length(app.t) error(i) = sqrt(mean((app.u_spectral(:,i) - app.u_fdm(:,i)).^2)); end % 绘制误差随时间的变化 cla(app.ErrorPlot); semilogy(app.ErrorPlot, app.t, error, 'k-', 'LineWidth', 1.5); % 设置标题和轴标签 if strcmp(currentMethod, '傅里叶谱方法') title(app.ErrorPlot, '傅里叶谱方法与有限差分法的L2误差'); else title(app.ErrorPlot, '有限差分法与傅里叶谱方法的L2误差'); end xlabel(app.ErrorPlot, '时间 t'); ylabel(app.ErrorPlot, 'L2误差'); grid(app.ErrorPlot, 'on'); % 计算速度对比 if strcmp(currentMethod, '傅里叶谱方法') method1 = '傅里叶谱方法'; method2 = '有限差分法'; time1 = app.solveTime; time2 = app.fdmSolveTime; else method1 = '有限差分法'; method2 = '傅里叶谱方法'; time1 = app.solveTime; time2 = app.fdmSolveTime; end % 更新性能指标文本 app.PerformanceMetricsTextArea.Value = { sprintf('%s计算时间: %.4f秒', method1, time1), sprintf('%s计算时间: %.4f秒', method2, time2), sprintf('加速比: %.2f倍', time2/time1), sprintf('最大误差: %.6e', max(error)), sprintf('平均误差: %.6e', mean(error)), sprintf('空间点数: %d', app.N), sprintf('时间步数: %d', length(app.t)), sprintf('CFL数: %.4f', app.alpha*app.dt/(app.L/app.N)^2) }; % 绘制性能对比条形图 cla(app.PerfBarAxes); methods = {method1, method2}; times = [time1, time2]; bar(app.PerfBarAxes, times); app.PerfBarAxes.XTickLabel = methods; ylabel(app.PerfBarAxes, '计算时间 (秒)'); title(app.PerfBarAxes, '计算时间对比'); % 添加数值标签 for i = 1:length(times) text(app.PerfBarAxes, i, times(i)*1.05, sprintf('%.4f秒', times(i)), ... 'HorizontalAlignment', 'center', 'FontSize', 9); end % 添加加速比标签 speedup = time2/time1; text(app.PerfBarAxes, 1.5, min(times)*0.5, sprintf('加速比: %.2f倍', speedup), ... 'HorizontalAlignment', 'center', 'FontWeight', 'bold', 'FontSize', 11); end end % 回调函数和界面事件处理 methods (Access = private) % 点击求解按钮的回调函数 function SolveButtonPushed(app, ~) % 更新状态 app.StatusLabel.Text = '计算中...'; try app.StatusLabel.FontColor = [0.8 0.4 0]; catch % 忽略错误,保持默认颜色 end drawnow; % 根据选择的方法进行求解 switch app.SpectralMethodDropDown.Value case '傅里叶谱方法' app.solve_fourier_spectral(); case '有限差分法' app.solve_fdm(); end % 设置时间滑块的范围 app.TimeSlider.Limits = [1, length(app.t)]; app.TimeSlider.Value = 1; % 更新状态 app.StatusLabel.Text = '计算完成!'; try app.StatusLabel.FontColor = [0 0.6 0]; catch % 忽略错误,保持默认颜色 end % 切换到可视化选项卡 app.TabGroup.SelectedTab = app.VisualizationTab; end % 时间滑块值改变的回调函数 function TimeSliderValueChanged(app, ~) app.updateVisualization(); end % 初始条件下拉列表值改变的回调函数 function ICDropDownValueChanged(app, ~) app.updateICPreview(); end % L值改变时的回调函数 function LEditFieldValueChanged(app, ~) app.updateICPreview(); end % 选择方法下拉列表值改变的回调函数 function SpectralMethodDropDownValueChanged(app, ~) % 如果选择有限差分法,默认使用狄利克雷边界 if strcmp(app.SpectralMethodDropDown.Value, '有限差分法') app.BCDropDown.Value = '狄利克雷边界'; else app.BCDropDown.Value = '周期性边界'; end end % 播放/暂停按钮的回调函数 function PlayButtonPushed(app, ~) % 如果正在播放,则暂停 if strcmp(app.PlayButton.Text, '暂停') app.PlayButton.Text = '播放'; try app.PlayButton.BackgroundColor = [0.3 0.8 0.3]; catch % 忽略错误,保持默认颜色 end stop(app.AnimationTimer); return; end % 否则开始播放 app.PlayButton.Text = '暂停'; try app.PlayButton.BackgroundColor = [0.8 0.3 0.3]; catch % 忽略错误,保持默认颜色 end % 获取动画速度 speed = 0.1; % 默认中速 switch app.AnimationSpeedDropDown.Value case '慢速' speed = 0.2; case '中速' speed = 0.1; case '快速' speed = 0.05; end % 创建并启动定时器 app.AnimationTimer = timer('ExecutionMode', 'fixedRate', ... 'Period', speed, ... 'TimerFcn', @(~,~) app.animationStep()); start(app.AnimationTimer); end % 动画步进函数 function animationStep(app) % 获取当前滑块值 currentIndex = app.TimeSlider.Value; % 如果已经到达最后一个时间点,则重置到开始 if currentIndex >= app.TimeSlider.Limits(2) currentIndex = app.TimeSlider.Limits(1); else % 否则前进到下一个时间点 currentIndex = currentIndex + 1; end % 更新滑块值 app.TimeSlider.Value = currentIndex; % 更新可视化 app.updateVisualization(); end end % 组件初始化和创建函数 methods (Access = private) % 创建UI组件 function createComponents(app) % 检查MATLAB版本,R2019b及以上支持uiaxes hasUIAxes = ~verLessThan('matlab', '9.7'); % R2019b版本号是9.7 % 创建主窗口 app.UIFigure = uifigure('Visible', 'off'); app.UIFigure.Position = [100, 100, 900, 700]; app.UIFigure.Name = '一维热传导方程谱方法求解器'; app.UIFigure.Color = [0.94 0.94 0.94]; % 创建选项卡组 app.TabGroup = uitabgroup(app.UIFigure); app.TabGroup.Position = [1, 1, 900, 700]; app.TabGroup.TabLocation = 'left'; % 创建设置选项卡 app.SettingsTab = uitab(app.TabGroup); app.SettingsTab.Title = '参数设置'; app.SettingsTab.BackgroundColor = [0.94 0.94 0.94]; % 创建面板来组织控件 paramPanel = uipanel(app.SettingsTab); paramPanel.Title = '计算参数'; paramPanel.FontWeight = 'bold'; paramPanel.FontSize = 14; paramPanel.Position = [30, 400, 400, 300]; paramPanel.BackgroundColor = [0.97 0.97 0.97]; bcPanel = uipanel(app.SettingsTab); bcPanel.Title = '条件设置'; bcPanel.FontWeight = 'bold'; bcPanel.FontSize = 14; bcPanel.Position = [460, 400, 400, 250]; bcPanel.BackgroundColor = [0.97 0.97 0.97]; methodPanel = uipanel(app.SettingsTab); methodPanel.Title = '数值方法设置'; methodPanel.FontWeight = 'bold'; methodPanel.FontSize = 14; methodPanel.Position = [30, 150, 830, 220]; methodPanel.BackgroundColor = [0.97 0.97 0.97]; % 创建空间区域设置组件 app.DomainLabel = uilabel(paramPanel); app.DomainLabel.Position = [20, 260, 200, 22]; app.DomainLabel.Text = '计算域设置'; app.DomainLabel.FontWeight = 'bold'; app.DomainLabel.FontSize = 12; app.LLabel = uilabel(paramPanel); app.LLabel.Position = [20, 240, 150, 22]; app.LLabel.Text = '域长度 L:'; app.LEditField = uieditfield(paramPanel, 'numeric'); app.LEditField.Position = [180, 240, 100, 22]; app.LEditField.Value = 1; app.LEditField.Limits = [0.1, 10]; app.LEditField.ValueDisplayFormat = '%.3f'; app.NLabel = uilabel(paramPanel); app.NLabel.Position = [20, 210, 150, 22]; app.NLabel.Text = '空间点数 N:'; app.NEditField = uieditfield(paramPanel, 'numeric'); app.NEditField.Position = [180, 210, 100, 22]; app.NEditField.Value = 128; app.NEditField.Limits = [16, 1024]; % 创建时间设置组件 app.TimeLabel = uilabel(paramPanel); app.TimeLabel.Position = [20, 180, 200, 22]; app.TimeLabel.Text = '时间设置'; app.TimeLabel.FontWeight = 'bold'; app.TimeLabel.FontSize = 12; app.TfinalLabel = uilabel(paramPanel); app.TfinalLabel.Position = [20, 150, 150, 22]; app.TfinalLabel.Text = '终止时间:'; app.TfinalEditField = uieditfield(paramPanel, 'numeric'); app.TfinalEditField.Position = [180, 150, 100, 22]; app.TfinalEditField.Value = 0.1; app.TfinalEditField.Limits = [0.001, 10]; app.TfinalEditField.ValueDisplayFormat = '%.4f'; app.dtLabel = uilabel(paramPanel); app.dtLabel.Position = [20, 120, 150, 22]; app.dtLabel.Text = '时间步长:'; app.dtEditField = uieditfield(paramPanel, 'numeric'); app.dtEditField.Position = [180, 120, 100, 22]; app.dtEditField.Value = 0.001; app.dtEditField.Limits = [1e-6, 1]; app.dtEditField.ValueDisplayFormat = '%.6f'; % 创建物理参数设置组件 app.PhysicalParamsLabel = uilabel(paramPanel); app.PhysicalParamsLabel.Position = [20, 90, 200, 22]; app.PhysicalParamsLabel.Text = '物理参数'; app.PhysicalParamsLabel.FontWeight = 'bold'; app.PhysicalParamsLabel.FontSize = 12; app.AlphaLabel = uilabel(paramPanel); app.AlphaLabel.Position = [20, 60, 150, 22]; app.AlphaLabel.Text = '热扩散系数 α:'; app.AlphaEditField = uieditfield(paramPanel, 'numeric'); app.AlphaEditField.Position = [180, 60, 100, 22]; app.AlphaEditField.Value = 0.01; app.AlphaEditField.Limits = [0.0001, 1]; app.AlphaEditField.ValueDisplayFormat = '%.5f'; % 创建初始条件和边界条件设置组件 app.ICLabel = uilabel(bcPanel); app.ICLabel.Position = [20, 180, 150, 22]; app.ICLabel.Text = '初始条件:'; app.ICLabel.FontWeight = 'bold'; app.ICDropDown = uidropdown(bcPanel); app.ICDropDown.Items = {'高斯脉冲', '阶跃函数', '正弦函数', '自定义'}; app.ICDropDown.Value = '高斯脉冲'; app.ICDropDown.Position = [180, 180, 180, 22]; app.ICDropDown.BackgroundColor = [1 1 1]; % 添加初始条件预览图 app.ICPreviewPanel = uipanel(bcPanel); app.ICPreviewPanel.Title = '初始条件预览'; app.ICPreviewPanel.Position = [20, 70, 340, 100]; % 根据MATLAB版本选择绘图函数 if hasUIAxes % 使用新版的uiaxes app.ICPreviewAxes = uiaxes(app.ICPreviewPanel); else % 使用传统的axes app.ICPreviewAxes = axes('Parent', app.ICPreviewPanel); end app.ICPreviewAxes.Position = [10, 10, 320, 70]; app.ICPreviewAxes.XGrid = 'on'; app.ICPreviewAxes.YGrid = 'on'; app.ICPreviewAxes.Box = 'on'; app.BCLabel = uilabel(bcPanel); app.BCLabel.Position = [20, 40, 150, 22]; app.BCLabel.Text = '边界条件:'; app.BCLabel.FontWeight = 'bold'; app.BCDropDown = uidropdown(bcPanel); app.BCDropDown.Items = {'周期性边界', '狄利克雷边界'}; app.BCDropDown.Value = '周期性边界'; app.BCDropDown.Position = [180, 40, 180, 22]; app.BCDropDown.BackgroundColor = [1 1 1]; % 在方法面板中创建数值方法设置组件 app.MethodLabel = uilabel(methodPanel); app.MethodLabel.Position = [20, 170, 150, 22]; app.MethodLabel.Text = '求解方法:'; app.MethodLabel.FontWeight = 'bold'; app.SpectralMethodDropDown = uidropdown(methodPanel); app.SpectralMethodDropDown.Items = {'傅里叶谱方法', '有限差分法'}; app.SpectralMethodDropDown.Value = '傅里叶谱方法'; app.SpectralMethodDropDown.Position = [180, 170, 180, 22]; app.SpectralMethodDropDown.BackgroundColor = [1 1 1]; app.CompareWithFDMCheckBox = uicheckbox(methodPanel); app.CompareWithFDMCheckBox.Text = '与另一种方法比较'; app.CompareWithFDMCheckBox.Position = [400, 170, 200, 22]; app.CompareWithFDMCheckBox.Value = true; app.CompareWithFDMCheckBox.FontWeight = 'bold'; % 添加方法说明文本 methodInfoText = uitextarea(methodPanel); methodInfoText.Position = [20, 50, 790, 100]; methodInfoText.Value = { '傅里叶谱方法: 适用于周期性边界条件,使用FFT进行加速计算,对光滑解具有指数收敛特性。', '有限差分法: 经典的数值方法,使用网格点上的差分近似导数,精度取决于网格密度。', '', '对于周期性边界条件,傅里叶谱方法通常更高效、更精确。', '对于狄利克雷边界条件,有限差分法实现简单,适用性广泛。' }; methodInfoText.FontSize = 11; methodInfoText.Editable = 'off'; % 创建求解按钮 - 使其更突出 app.SolveButton = uibutton(app.SettingsTab, 'push'); app.SolveButton.ButtonPushedFcn = createCallbackFcn(app, @SolveButtonPushed, true); app.SolveButton.Position = [375, 60, 150, 50]; app.SolveButton.Text = '开始求解'; app.SolveButton.FontSize = 16; app.SolveButton.FontWeight = 'bold'; try % 尝试设置按钮颜色,如果不支持则忽略 app.SolveButton.BackgroundColor = [0.3 0.6 0.9]; app.SolveButton.FontColor = [1 1 1]; catch % 忽略错误,保持默认颜色 end % 创建状态标签 app.StatusLabel = uilabel(app.SettingsTab); app.StatusLabel.Position = [350, 30, 200, 22]; app.StatusLabel.Text = '准备就绪'; app.StatusLabel.HorizontalAlignment = 'center'; % 创建可视化选项卡 app.VisualizationTab = uitab(app.TabGroup); app.VisualizationTab.Title = '可视化'; app.VisualizationTab.BackgroundColor = [0.94 0.94 0.94]; % 创建可视化控制面板 visControlPanel = uipanel(app.VisualizationTab); visControlPanel.Title = '可视化控制'; visControlPanel.FontWeight = 'bold'; visControlPanel.FontSize = 14; visControlPanel.Position = [30, 30, 830, 100]; visControlPanel.BackgroundColor = [0.97 0.97 0.97]; % 创建解的图形面板 visSolutionPanel = uipanel(app.VisualizationTab); visSolutionPanel.Title = '温度场分布'; visSolutionPanel.FontWeight = 'bold'; visSolutionPanel.FontSize = 14; visSolutionPanel.Position = [30, 150, 830, 500]; visSolutionPanel.BackgroundColor = [0.97 0.97 0.97]; % 创建解的图形 if hasUIAxes % 使用新版的uiaxes app.SolutionPlot = uiaxes(visSolutionPanel); else % 使用传统的axes app.SolutionPlot = axes('Parent', visSolutionPanel); end app.SolutionPlot.Position = [20, 20, 790, 460]; title(app.SolutionPlot, '温度场分布'); xlabel(app.SolutionPlot, '空间坐标 x'); ylabel(app.SolutionPlot, '温度 u(x,t)'); grid(app.SolutionPlot, 'on'); app.SolutionPlot.Box = 'on'; app.SolutionPlot.FontSize = 12; % 创建时间滑块 app.TimeSlider = uislider(visControlPanel); app.TimeSlider.Position = [150, 50, 500, 3]; app.TimeSlider.Limits = [1, 100]; app.TimeSlider.Value = 1; app.TimeSlider.ValueChangedFcn = createCallbackFcn(app, @TimeSliderValueChanged, true); % 添加播放/暂停按钮 app.PlayButton = uibutton(visControlPanel, 'push'); app.PlayButton.Position = [50, 45, 80, 30]; app.PlayButton.Text = '播放'; try % 尝试设置按钮颜色,如果不支持则忽略 app.PlayButton.BackgroundColor = [0.3 0.8 0.3]; app.PlayButton.FontColor = [1 1 1]; catch % 忽略错误,保持默认颜色 end app.PlayButton.ButtonPushedFcn = createCallbackFcn(app, @PlayButtonPushed, true); % 添加时间步长控制 app.AnimationSpeedLabel = uilabel(visControlPanel); app.AnimationSpeedLabel.Position = [670, 50, 80, 22]; app.AnimationSpeedLabel.Text = '动画速度:'; app.AnimationSpeedDropDown = uidropdown(visControlPanel); app.AnimationSpeedDropDown.Items = {'慢速', '中速', '快速'}; app.AnimationSpeedDropDown.Value = '中速'; app.AnimationSpeedDropDown.Position = [750, 50, 60, 22]; app.CurrentTimeLabel = uilabel(visControlPanel); app.CurrentTimeLabel.Position = [330, 15, 200, 22]; app.CurrentTimeLabel.Text = '当前时间: 0.0000 / 0.1000'; app.CurrentTimeLabel.HorizontalAlignment = 'center'; % 创建误差分析选项卡 app.ErrorAnalysisTab = uitab(app.TabGroup); app.ErrorAnalysisTab.Title = '误差分析'; app.ErrorAnalysisTab.BackgroundColor = [0.94 0.94 0.94]; % 创建误差图形面板 errorPanel = uipanel(app.ErrorAnalysisTab); errorPanel.Title = '误差分析'; errorPanel.FontWeight = 'bold'; errorPanel.FontSize = 14; errorPanel.Position = [30, 350, 830, 300]; errorPanel.BackgroundColor = [0.97 0.97 0.97]; % 创建性能指标面板 perfPanel = uipanel(app.ErrorAnalysisTab); perfPanel.Title = '性能指标'; perfPanel.FontWeight = 'bold'; perfPanel.FontSize = 14; perfPanel.Position = [30, 30, 830, 300]; perfPanel.BackgroundColor = [0.97 0.97 0.97]; % 创建误差图形 if hasUIAxes % 使用新版的uiaxes app.ErrorPlot = uiaxes(errorPanel); else % 使用传统的axes app.ErrorPlot = axes('Parent', errorPanel); end app.ErrorPlot.Position = [20, 20, 790, 260]; title(app.ErrorPlot, '谱方法与有限差分法的误差对比'); xlabel(app.ErrorPlot, '时间 t'); ylabel(app.ErrorPlot, 'L2误差'); grid(app.ErrorPlot, 'on'); app.ErrorPlot.Box = 'on'; app.ErrorPlot.YScale = 'log'; app.ErrorPlot.FontSize = 12; % 创建性能指标文本区域 app.PerformanceMetricsTextArea = uitextarea(perfPanel); app.PerformanceMetricsTextArea.Position = [20, 100, 400, 180]; app.PerformanceMetricsTextArea.Value = {'性能指标将在求解后显示'}; app.PerformanceMetricsTextArea.FontSize = 12; app.PerformanceMetricsTextArea.Editable = 'off'; % 创建性能指标可视化 if hasUIAxes % 使用新版的uiaxes app.PerfBarAxes = uiaxes(perfPanel); else % 使用传统的axes app.PerfBarAxes = axes('Parent', perfPanel); end app.PerfBarAxes.Position = [450, 100, 360, 180]; title(app.PerfBarAxes, '计算时间对比'); app.PerfBarAxes.XTickLabelRotation = 45; app.PerfBarAxes.Box = 'on'; app.PerfBarAxes.FontSize = 11; % 创建帮助选项卡 app.HelpTab = uitab(app.TabGroup); app.HelpTab.Title = '帮助'; app.HelpTab.BackgroundColor = [0.94 0.94 0.94]; % 添加帮助信息 helpTextArea = uitextarea(app.HelpTab); helpTextArea.Position = [30, 30, 830, 620]; helpTextArea.Value = { '一维热传导方程谱方法求解器使用说明', '==========================================', '', '1. 参数设置选项卡:', ' - 计算域参数: 设置空间域长度和离散点数', ' - 时间参数: 设置仿真终止时间和时间步长', ' - 物理参数: 设置热扩散系数', ' - 初始条件: 选择初始温度分布', ' - 边界条件: 选择边界类型', ' - 求解方法: 选择傅里叶谱方法或有限差分法', '', '2. 可视化选项卡:', ' - 使用时间滑块观察不同时刻的温度分布', ' - 使用播放按钮自动播放温度场演化过程', ' - 调整动画速度控制播放速率', '', '3. 误差分析选项卡:', ' - 查看谱方法与有限差分法的误差对比', ' - 查看性能指标,包括计算时间和加速比', '', '算法说明:', '=========', '热传导方程: ∂u/∂t = α ∂²u/∂x²', '', '傅里叶谱方法原理:', '1. 将解展开为傅里叶级数', '2. 在频域中计算空间导数', '3. 使用FFT进行物理空间和频域之间的变换', '4. 采用时间积分方法求解', '', '有限差分法: 经典的数值方法,使用网格点上的差分近似导数,精度取决于网格密度。', '', '更多信息请参考文档。' }; helpTextArea.FontSize = 12; helpTextArea.Editable = 'off'; % 选择第一个选项卡作为默认选项卡 app.TabGroup.SelectedTab = app.SettingsTab; end end % 公共方法 methods (Access = public) % 构造函数 function app = HeatEquationSolver % 创建UI组件 createComponents(app) % 初始化属性 app.isSolved = false; % 设置回调函数 app.ICDropDown.ValueChangedFcn = createCallbackFcn(app, @ICDropDownValueChanged, true); app.LEditField.ValueChangedFcn = createCallbackFcn(app, @LEditFieldValueChanged, true); app.SpectralMethodDropDown.ValueChangedFcn = createCallbackFcn(app, @SpectralMethodDropDownValueChanged, true); % 初始化初始条件预览 app.updateICPreview(); % 显示UI app.UIFigure.Visible = 'on'; end % 析构函数 - 清理定时器 function delete(app) % 停止并删除动画定时器(如果存在) if ~isempty(app.AnimationTimer) && isvalid(app.AnimationTimer) stop(app.AnimationTimer); delete(app.AnimationTimer); end % 删除组件 delete(app.UIFigure); end end end代码实现后,选用傅里叶解为主方法同时勾选与另一种方法对比时,主方法的可视化动画无法实现怎么修改
最新发布
05-27
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值