Spinner Model Controls(一)

本文介绍了JSpinner组件的使用方法,包括创建与配置JSpinner、监听JSpinner事件以及自定义JSpinner外观等内容。JSpinner组件结合了JList或JComboBox与JFormattedTextField的功能,允许用户从预定义值集合中选择或手动输入值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前一章中,我们了解了如何使用基本的列表组件:JList与JComboBox。在本章中,我们将会开始探讨JDK 1.4版本所引入的JSpinner组件。

14.1 JSpinner类

JSpinner的作用类似于JList或是JComboBox组件与JFormattedTextField的结合。在JList与JComboBox控件中,用户可以由一个预定义的值集合中选择输入。JSpinner允许这种选择类型。组件的另一半是JFormattedTextField。如何显示或是输入值并不由列表单元渲染器来控制,类似于JList;相反,我们获取JFormattedTextField用于输入并且在旁边有一对箭头用于在文本域可用的不同值之间进行浏览。

图14-1显示了用于不同的输入类型的微调控件的样子。图14-1的顶部是一个以法国星期显示提供给SpinnerListModel的JSpinner。在中部,我们具有一个依赖SpinnerDateModel类的JSpinner。在底部是一个带有SpinnerNumberModel的JSpinner使用示例。正如我们在本章稍后将会了解到的,每一个都以其自己的方式进行工作。


当创建并操作JSpinner组件时会涉及到许多类,首先就是JSpinner类本身。所涉及到的两个基本类集合是包含用于控件可选条目集合的SpinnerModel接口,以及用于捕获所有选择的JSpinnner.DefaultEditor实现。幸运的是,所涉及到的其他许多类在幕后工作,所以,例如,一旦我们在SpinnerNumberModel中提供一个数字范围并且与其模型相叛逆,我们的工作实质上就完成了。

14.1.1 创建JSpinner组件

JSpinner类包含两个用于初始化组件的构造函数:

public JSpinner()
JSpinner spinner = new JSpinner();
public JSpinner(SpinnerModel model)
SpinnerModel model = new SpinnerListModel(args);
JSpinner spinner = new JSpinner(model);

我们可以由无数据模型开始,并使用JSpinner的方法在稍后进行关联。或者是我们可以由一个完全的数据模型开始,数据模型是SpinnerModel接口的实现,其中有三个主要类:SpinnerDateModel,SpinnerListModel与SpinnerNumberModel,及其抽象父类AbstractSpinnerModel。如果我们没有指定一个模型,则使用SpinnerNumberModel。尽管组件的渲染器与编辑器是JFormattedTextField,编辑基本是通过一系列的JSpinner内联类来完成的:DateEditor,ListEditor与NumberFormat,而其支持类则位于父类DefaultEditor中。

14.1.2 JSpinner属性

除了创建JSpinner对象以外,我们当然也可以通过表14-1中所列出的属性对其进行重新配置。


value属性使得我们可以修改组件的当前设置。nextValue与previosValue属性使得我们可以在不同方向的model的条目中进行选择,而无需修改程序本身的选择。

14.1.3 使用ChnageListener监听JSpinner事件

JSpinner只直接支持一种事件监听器:ChangeListener。在其他情况下,当为相关组件调用commitEdit()方法时,监听器会得到通知,告诉我们微调器的值发生了变化。为了进行演示,列表14-1向生成图14-1的源程序关联了一个自定义的ChangeListener。

package swingstudy.ch13;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.text.DateFormatSymbols;
import java.util.Locale;
 
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerDateModel;
import javax.swing.SpinnerListModel;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
 
public class SpinnerSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("JSpinner Sample");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				DateFormatSymbols symbols = new DateFormatSymbols(Locale.FRENCH);
				ChangeListener listener = new ChangeListener() {
					public void stateChanged(ChangeEvent e) {
						System.out.println("Source: "+e.getSource());
					}
				};
 
				String days[] = symbols.getWeekdays();
				SpinnerModel model1 = new SpinnerListModel(days);
				JSpinner spinner1 = new JSpinner(model1);
				spinner1.addChangeListener(listener);
				JLabel label1 = new JLabel("French Days/List");
				JPanel panel1 = new JPanel(new BorderLayout());
				panel1.add(label1, BorderLayout.WEST);
				panel1.add(spinner1, BorderLayout.CENTER);
				frame.add(panel1, BorderLayout.NORTH);
 
				SpinnerModel model2 = new SpinnerDateModel();
				JSpinner spinner2 = new JSpinner(model2);
				spinner2.addChangeListener(listener);
				JLabel label2 = new JLabel("Dates/Date");
				JPanel panel2 = new JPanel(new BorderLayout());
				panel2.add(label2, BorderLayout.WEST);
				panel2.add(spinner2, BorderLayout.CENTER);
				frame.add(panel2, BorderLayout.CENTER);
 
				SpinnerModel model3 = new SpinnerNumberModel();
				JSpinner spinner3 = new JSpinner(model3);
				spinner3.addChangeListener(listener);
				JLabel label3 = new JLabel("Numbers");
				JPanel panel3 = new JPanel(new BorderLayout());
				panel3.add(label3, BorderLayout.WEST);
				panel3.add(spinner3, BorderLayout.CENTER);
				frame.add(panel3, BorderLayout.SOUTH);
 
				frame.setSize(200, 90);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}

运行这个程序演示了监听器的使用。

14.1.4 自定义JSpinner观感

类似于所有的Swing组件,JSpinner控件在每一个系统定义的观感类型下都会有不同的外观,如图14-2所示。组件的基本外观看起来像一个文本域;不同在于箭头的绘制。


表14-2中列出了JSpinner的11个UIResource属性集合。这个属性局限于绘制文本域与箭头。


<template> <view class="chat-container"> <!-- 消息列表区域 --> <view class="message-list" ref="messageList"> <view v-for="(message, index) in messages" :key="index" :class="['message', message.isUser ? 'user-message' : 'system-message']"> <!-- 用户消息 --> <view v-if="message.isUser" class="message-content"> <view class="avatar user-avatar">👤</view> <view class="bubble"> <p>{{ message.content }}</p> <view v-if="message.taskId" class="task-info"> <span class="tag">{{ message.type === 'image' ? '图片生成' : '视频生成' }}</span> <span class="status" :class="message.status">{{ getStatusText(message.status) }}</span> </view> </view> </view> <!-- 系统消息 --> <view v-else class="message-content"> <view class="avatar system-avatar">🤖</view> <view class="bubble"> <!-- 生成状态 --> <view v-if="message.status === 'pending'" class="generating"> <view class="loading-spinner"></view> <p>正在生成{{ message.type === 'image' ? '图片' : '视频' }},请稍候...</p> </view> <!-- 生成结果 --> <view v-if="message.status === 'completed'"> <view v-if="message.type === 'image'"> <image style="width: 50rpx; height: 50rpx;" :src="message.url" alt="生成的图片" @click="previewImage(message.url)" /> <p class="hint">点击图片查看大图</p> </view> <view v-else-if="message.type === 'video'"> <video controls :src="message.url"></video> <p class="hint">视频生成完成</p> </view> </view> <!-- 错误提示 --> <view v-if="message.status === 'error'" class="error-message"> <p>⚠️ 生成失败: {{ message.error }}</p> </view> </view> </view> </view> </view> <!-- 输入区域 --> <view class="input-area"> <textarea v-model="inputText" placeholder="输入描述文字..." @keyup.enter="sendMessage"></textarea> <view class="action-buttons"> <button @click="generateImage" :disabled="!inputText.trim()">生成图片</button> <button @click="generateVideo" :disabled="!inputText.trim()">生成视频</button> </view> </view> <!-- 任务队列悬浮窗 --> <view class="task-queue" v-if="taskQueue.length > 0"> <h4>生成队列 ({{ processingTask ? '处理中' : '等待中' }})</h4> <ul> <li v-for="(task, index) in taskQueue" :key="task.id" :class="['task-item', index === 0 && processingTask ? 'processing' : '']"> <span class="task-type">{{ task.type === 'image' ? '🖼️' : '🎬' }}</span> <span class="task-prompt">{{ task.prompt }}</span> <span class="task-status">{{ getQueueStatus(index) }}</span> </li> </ul> </view> </view> </template> <script> export default { data() { return { inputText: '', messages: [], taskQueue: [], processingTask: null, nextTaskId: 1 }; }, mounted() { // 添加初始欢迎消息 this.addSystemMessage('欢迎使用AI生成助手!请描述您想要生成的图片或视频内容。'); }, methods: { // 添加用户消息 addUserMessage(content, type) { this.messages.push({ id: Date.now(), isUser: true, content, type, status: 'queued', taskId: this.nextTaskId++ }); this.scrollToBottom(); }, // 添加系统消息 addSystemMessage(content, type = 'text', status = 'completed') { this.messages.push({ id: Date.now(), isUser: false, content, type, status }); this.scrollToBottom(); }, // 滚动到底部 scrollToBottom() { this.$nextTick(() => { const container = this.$refs.messageList; container.scrollTop = container.scrollHeight; }); }, // 生成图片 generateImage() { if (!this.inputText.trim()) return; const prompt = this.inputText.trim(); this.inputText = ''; // 添加用户消息 this.addUserMessage(prompt, 'image'); // 添加到任务队列 this.addToQueue({ type: 'image', prompt, taskId: this.nextTaskId - 1 }); }, // 生成视频 generateVideo() { if (!this.inputText.trim()) return; const prompt = this.inputText.trim(); this.inputText = ''; // 添加用户消息 this.addUserMessage(prompt, 'video'); // 添加到任务队列 this.addToQueue({ type: 'video', prompt, taskId: this.nextTaskId - 1 }); }, // 添加到任务队列 addToQueue(task) { this.taskQueue.push(task); // 添加系统提示消息 this.addSystemMessage(`您的${task.type === 'image' ? '图片' : '视频'}生成任务已加入队列`, 'text'); // 如果没有正在处理的任务,开始处理 if (!this.processingTask) { this.processQueue(); } }, // 处理任务队列 async processQueue() { if (this.taskQueue.length === 0) { this.processingTask = null; return; } // 获取第个任务 this.processingTask = this.taskQueue[0]; // 更新消息状态为处理中 this.updateMessageStatus(this.processingTask.taskId, 'pending'); try { // 模拟生成过程(实际应调用API) const result = await this.generateContent(this.processingTask); // 添加系统结果消息 this.addSystemMessage(result.url, this.processingTask.type, 'completed'); // 更新消息状态为完成 this.updateMessageStatus(this.processingTask.taskId, 'completed'); // 从队列中移除已完成任务 this.taskQueue.shift(); // 处理下个任务 this.processQueue(); } catch (error) { // 添加错误消息 this.addSystemMessage(`生成失败: ${error.message}`, 'text', 'error'); // 更新消息状态为错误 this.updateMessageStatus(this.processingTask.taskId, 'error', error.message); // 从队列中移除失败任务 this.taskQueue.shift(); // 处理下个任务 this.processQueue(); } }, // 更新消息状态 updateMessageStatus(taskId, status, error = '') { const message = this.messages.find(m => m.taskId === taskId); if (message) { message.status = status; if (error) message.error = error; } }, // 生成内容(模拟) generateContent(task) { return new Promise((resolve, reject) => { // 模拟API调用延迟 setTimeout(() => { if (Math.random() > 0.1) { // 90%成功率 resolve({ url: task.type === 'image' ? `https://picsum.photos/400/300?random=${Math.floor(Math.random() * 1000)}` : 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' }); } else { reject(new Error('AI生成服务暂时不可用')); } }, task.type === 'image' ? 3000 : 6000); // 图片3秒,视频6秒 }); }, // 预览图片 previewImage(url) { // 实际项目中应使用图片预览组件 window.open(url, '_blank'); }, // 获取状态文本 getStatusText(status) { const statusMap = { queued: '队列中', pending: '生成中', completed: '已完成', error: '失败' }; return statusMap[status] || status; }, // 获取队列状态 getQueueStatus(index) { if (index === 0 && this.processingTask) return '处理中'; return `等待 ${index}`; } } }; </script> <style scoped> .chat-container { display: flex; flex-direction: column; height: 100vh; max-width: 800px; margin: 0 auto; background-color: #f0f2f5; position: relative; } .message-list { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; } .message-content { display: flex; max-width: 80%; } .user-message .message-content { margin-left: auto; flex-direction: row-reverse; } .system-message .message-content { margin-right: auto; } .avatar { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; margin: 0 10px; } .user-avatar { background-color: #1890ff; color: white; } .system-avatar { background-color: #52c41a; color: white; } .bubble { padding: 12px 15px; border-radius: 18px; position: relative; max-width: 100%; word-break: break-word; } .user-message .bubble { background-color: #1890ff; color: white; border-bottom-right-radius: 5px; } .system-message .bubble { background-color: white; color: #333; border-bottom-left-radius: 5px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .task-info { margin-top: 8px; font-size: 0.8em; opacity: 0.8; display: flex; align-items: center; gap: 8px; } .tag { background: rgba(255, 255, 255, 0.2); padding: 2px 6px; border-radius: 4px; } .status { padding: 2px 6px; border-radius: 4px; } .status.queued { background-color: #f0f0f0; color: #666; } .status.pending { background-color: #e6f7ff; color: #1890ff; } .status.completed { background-color: #f6ffed; color: #52c41a; } .status.error { background-color: #fff2f0; color: #ff4d4f; } .generating { display: flex; align-items: center; gap: 10px; } .loading-spinner { width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-top-color: #1890ff; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .input-area { padding: 15px; background-color: white; border-top: 1px solid #e8e8e8; border-radius: 15px; } textarea { width: 95%; min-height: 60px; padding: 10px; border: 1px solid #d9d9d9; border-radius: 4px; resize: none; font-family: inherit; border-radius: 15px; } .action-buttons { display: flex; gap: 10px; margin-top: 10px; } button { flex: 1; /* padding: 10px; */ background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; } button:disabled { background-color: #bfbfbf; cursor: not-allowed; } button:hover:not(:disabled) { background-color: #40a9ff; } .task-queue { position: absolute; top: 80rpx; right: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); padding: 15px; max-width: 300px; z-index: 100; } .task-queue h4 { margin-top: 0; margin-bottom: 10px; color: #333; } .task-item { padding: 8px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 10px; } .task-item:last-child { border-bottom: none; } .task-type { font-size: 1.2em; } .task-prompt { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .task-status { font-size: 0.85em; color: #666; } .task-item.processing .task-status { color: #1890ff; font-weight: bold; } image, video { max-width: 100%; border-radius: 8px; display: block; margin-top: 5px; } video { max-width: 400px; } .hint { font-size: 0.8em; color: #666; margin-top: 5px; } .error-message { color: #ff4d4f; } </style> 你看看 为什么系统消息有个空的气泡 就是我点击生成图片之后生成成功就会出现个 开始没有发消息也出现了个 解决
08-05
以上mrviewer.vue中的本草關聯功能以前述病態關聯功能為範例進行修改, mrmncrud.vue: <template> <div class="mncrud-container"> <div class="mncrud-controls"> <input v-model="searchQuery" placeholder="搜索本草名稱..." class="search-input" @input="filterMNTags" /> </div> <!-- 创建表单 --> <div class="form-section"> <form @submit.prevent="createMNTag" class="create-form"> <input v-model="newMNTag.mnname" placeholder="輸入新本草名稱" required class="form-input" :disabled="isLoading" /> <button type="submit" class="form-btn create-btn" :disabled="isLoading">創建</button> </form> </div> <div class="mncrud-content"> <div v-if="isLoading" class="loading"> <div class="loading-spinner"></div> <span>加載中...</span> </div> <div v-else> <!-- 标签列表 --> <div class="mncrud-list" v-if="filteredTags.length > 0"> <div v-for="tag in filteredTags" :key="tag.id" class="mncrud-item"> <div class="tag-info"> <span class="tag-id">ID: {{ tag.id }}</span> <span class="tag-name">{{ tag.mnname }}</span> </div> <div class="tag-actions"> <button @click="editMNTag(tag)" class="action-btn edit-btn">編輯</button> <button @click="deleteMNTag(tag.id)" class="action-btn delete-btn">刪除</button> </div> </div> </div> <div v-else class="no-tags"> 沒有找到匹配的本草標籤 </div> </div> </div> <!-- 编辑模态框 --> <div v-if="editingTag" class="edit-modal"> <div class="modal-content"> <h3>編輯本草名稱</h3> <form @submit.prevent="updateMNTag"> <input v-model="editingTag.mnname" required class="form-input" :disabled="isLoading" /> <div class="modal-actions"> <button type="submit" class="form-btn save-btn">保存</button> <button type="button" @click="cancelEdit" class="form-btn cancel-btn">取消</button> </div> </form> </div> </div> </div> </template> <script> export default { name: 'mrmncrud', data() { return { mnTags: [], filteredTags: [], searchQuery: '', newMNTag: { mnname: '' }, editingTag: null, isLoading: false }; }, methods: { async fetchMNTags() { this.isLoading = true; try { const response = await fetch('MNTag/?format=json'); if (!response.ok) throw new Error('获取数据失败'); this.mnTags = await response.json(); this.filterMNTags(); } catch (error) { console.error('获取本草标签失败:', error); alert('加载数据失败,请重试'); } finally { this.isLoading = false; } }, filterMNTags() { const query = this.searchQuery.trim().toLowerCase(); if (!query) { this.filteredTags = [...this.mnTags]; } else { this.filteredTags = this.mnTags.filter(tag => tag.mnname.toLowerCase().includes(query) || tag.id.toString().includes(query) ); } }, async createMNTag() { if (!this.newMNTag.mnname.trim()) { alert('本草名称不能为空'); return; } this.isLoading = true; try { const response = await fetch('MNTag/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.newMNTag), }); if (!response.ok) throw new Error('创建失败'); await this.fetchMNTags(); this.newMNTag.mnname = ''; this.$emit('tag-updated'); } catch (error) { console.error('创建本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, editMNTag(tag) { this.editingTag = { ...tag }; }, async updateMNTag() { if (!this.editingTag.mnname.trim()) { alert('本草名称不能为空'); return; } this.isLoading = true; try { const response = await fetch(`MNTag/${this.editingTag.id}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.editingTag), }); if (!response.ok) throw new Error('更新失败'); await this.fetchMNTags(); this.editingTag = null; this.$emit('tag-updated'); } catch (error) { console.error('更新本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } }, cancelEdit() { this.editingTag = null; }, async deleteMNTag(id) { if (!confirm('确定要删除这个本草标签吗?')) return; this.isLoading = true; try { const response = await fetch(`MNTag/${id}/?format=json`, { method: 'DELETE' }); if (!response.ok) throw new Error('删除失败'); await this.fetchMNTags(); this.$emit('tag-updated'); } catch (error) { console.error('删除本草标签失败:', error); alert(error.message); } finally { this.isLoading = false; } } }, mounted() { this.fetchMNTags(); } }; </script> <style scoped> .mncrud-container { height: 100%; display: flex; flex-direction: column; } .mncrud-controls { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .search-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .mncrud-content { flex: 1; overflow-y: auto; padding: 10px; } .form-section { padding: 10px; background: #f5f5f5; border-bottom: 1px solid #ddd; } .create-form { display: flex; gap: 10px; } .form-input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .form-btn { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } .create-btn { background-color: #4CAF50; color: white; } .mncrud-list { display: flex; flex-direction: column; gap: 10px; } .mncrud-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: white; border: 1px solid #eee; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .tag-info { display: flex; flex-direction: column; } .tag-id { font-size: 0.8rem; color: #666; } .tag-name { font-weight: 500; } .tag-actions { display: flex; gap: 5px; } .action-btn { padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9rem; } .edit-btn { background-color: #2196F3; color: white; } .delete-btn { background-color: #f44336; color: white; } .no-tags { padding: 20px; text-align: center; color: #666; font-style: italic; } .edit-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; padding: 20px; border-radius: 8px; width: 90%; max-width: 400px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 15px; } .save-btn { background-color: #4CAF50; color: white; } .cancel-btn { background-color: #9E9E9E; color: white; } .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; } .loading-spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top: 4px solid #3498db; width: 30px; height: 30px; animation: spin 1s linear infinite; margin-bottom: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>
08-04
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值