使用IntelliJ创建Compose Desktop项目,JDK这里选择11以上
然后点击“Next”按钮,这将会跳转至确认 Compose 模块的界面。现在点击“Finish”按钮,IntelliJ 将通过自动下载适当的 gradle 为你配置整个项目。
如原型图所示,ui分为上下结构的设备状态,两个相机预览区域,和运行日志
上下布局采用Column,其中Modifier以高阶函数链式调用的形式支持Modifier.size().background().padding()等诸多属性,如下所示
Column(Modifier.background(MaterialTheme.colors.surface).fillMaxSize().padding(padding)) {
//设备状态
message(deviceStatus.value)
//两个相机预览区域
Row {
cameraPreview(this@Window, mvCamreraPanel)
cameraPreview(this@Window, javacvPanel)
}
//运行日志
messageList(visible.value, data.value) { visible.value = !visible.value }
}
很简单采用横向控件Row,并设置垂直居中即可
@Composable
fun message(deviceStatus: DeviceStatus) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("设备状态: ${deviceStatus.deviceStatus}", modifier = Modifier.padding(end = padding), fontSize = fontSize)
Text("相机状态: ${deviceStatus.cameraStatus}", modifier = Modifier.padding(end = padding), fontSize = fontSize)
Text("联网状态: ${deviceStatus.netStatus}", fontSize = fontSize)
}
}
其中SwingPanel是compose用来兼容swing的组件
@Composable
fun cameraPreview(frameWindowScope: FrameWindowScope, camreraPanel: CamreraPanel) {
frameWindowScope.window.addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
camreraPanel.stop()
}
})
val padding = if (camreraPanel is JavacvCameraPanel) 0.dp else padding
Column(Modifier.padding(top = 5.dp).wrapContentWidth()) {
cameraMessage("俯视机位预览") { camreraPanel.saveImage() }
SwingPanel(
modifier = Modifier.padding(top = 5.dp, end = padding)
.size(Dp(previewWidth.toFloat()), Dp(previewHeight.toFloat())),
factory = { camreraPanel.getPanel() })
}
}
主要采用LazyColumn,实现列表展示
@Composable
fun messageList(more: Boolean, messages: List<Message>, onClick: () -> Unit) {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text("运行日志: ", modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), fontSize = fontSize)
TextButton(
modifier = Modifier.size(50.dp, 30.dp),
shape = RoundedCornerShape(50),
contentPadding = PaddingValues(vertical = 3.dp),
onClick = onClick,
) {
val text = if (more) "放大" else "缩小"
Text(text, fontSize = fontSize)
}
}
LazyColumn(
modifier = Modifier.border(1.dp, color = Color.Gray)
.fillMaxSize()
.padding(10.dp)
) {
items(
count = messages.size,
key = { index -> messages[index] }
) { index ->
Text(messages[index].message, fontSize = 12.sp)
}
}
}
打开usb摄像头的库有javacv 和 webcam-capture,
webcam-capture在windows上发现预览非常卡顿,webcam-capture对摄像头预览做了太多封装,具体问题就不分析了
@Composable
fun camera2Preview(frameWindowScope: FrameWindowScope) {
val webcam = Webcam.getDefault()?.apply {
setCustomViewSizes(Dimension(4000, 3000), Dimension(1920, 1080), Dimension(1280, 720))
viewSize = WebcamResolution.FHD.size
}
frameWindowScope.window.addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
webcam?.close()
}
})
Column(Modifier.padding(top = 5.dp).wrapContentWidth()) {
cameraMessage("侧视机位预览") {
CoroutineScope(Dispatchers.IO).launch {
WebcamUtils.capture(webcam, System.currentTimeMillis().toString(), ImageUtils.FORMAT_PNG)
JOptionPane.showMessageDialog(null, "拍照成功")
}
}
SwingPanel(
modifier = Modifier.padding(top = 5.dp, end = 15.dp).border(1.dp, color = Color.Gray)
.size(Dp(previewWidth.toFloat()), Dp(previewHeight.toFloat())),
factory = {
WebcamPanel(webcam, true).apply {
isFPSDisplayed = true
isImageSizeDisplayed = true
isFPSDisplayed = true
drawMode = WebcamPanel.DrawMode.FILL
}
})
}
}
使用javacv做相机预览,javacv提供了CanvasFrame作为一个窗口预览,只用采用while循环获取摄像头输出的每一帧canvas.showImage(grabber.grab())即可显示图像
OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(1);//新建opencv抓取器,一般的电脑和移动端设备中摄像头默认序号是0,不排除其他情况
grabber.start();//开始获取摄像头数据
CanvasFrame canvas = new CanvasFrame("摄像头预览");//新建一个预览窗口
canvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//窗口是否关闭
while (canvas.isDisplayable()) {
/*获取摄像头图像并在窗口中显示,这里Frame frame=grabber.grab()得到是解码后的视频图像*/
canvas.showImage(grabber.grab());
}
grabber.close();//停止抓取
而我们需要将图像预览在compose desktop上,那么就需要grabber.grab()获取每一帧,显示在自己的控件上,
这里我采用的swing的JLabel
显示图像,就需要用到SwingPanel了
将Swing组件添加到你的Compose UI中,需要在SwingPanel
部件的factory
lambda参数中创建JComponent,示例如下:
SwingPanel(
background = Color.White,
modifier = Modifier.size(270.dp, 90.dp),
factory = {
JPanel().apply {
setLayout(BoxLayout(this, BoxLayout.Y_AXIS))
add(actionButton("1. Swing Button: decrement", dec))
add(actionButton("2. Swing Button: decrement", dec))
add(actionButton("3. Swing Button: decrement", dec))
}
}
)
现在可以实现摄像头预览框了:
@Composable
fun cvCameraPreview(frameWindowScope: FrameWindowScope) {
val javacvPanel = JavacvCameraPanel()
frameWindowScope.window.addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
javacvPanel.stop()
}
})
Column(Modifier.padding(top = 5.dp).wrapContentWidth()) {
cameraMessage("侧视机位预览") { javacvPanel.saveImage() }
SwingPanel(
modifier = Modifier.padding(top = 5.dp)
.size(Dp(previewWidth.toFloat()), Dp(previewHeight.toFloat())),
factory = {
javacvPanel
})
}
}
其中JavacvCameraPanel为javacv预览的JLabel
class JavacvCameraPanel : JLabel(), CamreraPanel {
private var job: Job? = null
private var grabber: OpenCVFrameGrabber? = null
init {
addcvLine()
start()
}
override fun getPanel() = this
override fun start() {
job = CoroutineScope(Dispatchers.IO).launch {
tryCatchFor {
grabber = OpenCVFrameGrabber.createDefault(0).apply {
imageWidth = pictureWidth
imageHeight = pictureHeight
start()
}
do {
getBufferedImage(grabber?.grab()).let {
icon = ImageIcon(createThumbnail(it, previewWidth, previewHeight))
}
// 每40毫秒刷新视频,一秒25帧
Thread.sleep(40)
} while (isVisible && isActive && grabber != null)
}
}
}
}
看看预览界面:
其中还涉及到对每一帧图像做居中裁剪,否则预览就是1920*1080这样矩形预览框
另一个相机预览展示同理,采用海康sdk获取摄像头预览每一帧,显示在JLabel
边框和虚线实现,由于也是第一次使用swing,查找api花了很久,其实继续添加JLabel就能实现
fun JLabel.addcvLine() {
//边框
add(JLabel("").apply {
border = BorderFactory.createLineBorder(Color.BLACK)
setBounds(0, 0, previewWidth, previewHeight)
}, BorderLayout.CENTER)
//横线
add(JLabel("").apply {
border = compoundBorder()
setBounds(0, previewHeight / 2, previewWidth, 1)
}, BorderLayout.CENTER)
//竖线
add(JLabel("").apply {
border = compoundBorder()
setBounds(previewWidth / 2, 0, 1, previewHeight)
}, BorderLayout.CENTER)
}
在compose之前,声明式ui在flutter和swift中尝试过,所以画简单的ui没什么特别大感觉
踩坑:
1,mac下打包不支持打包exe文件,必须使用windows才可以
2,对摄像头曝光,缩放等在mac设置无效,换到windows上面就可以
3,compose desktop没有对桌面应用升级的支持,加上javacv打包出来800M+
4,似乎不支持设置文本复制?